4. Dockerfile 编写基础:FROM、RUN、CMD、ENTRYPOINT 指令详解及最佳实践
Dockerfile 是啥?说白了就是一份构建说明书。你把环境依赖、安装步骤、启动命令都写在里面,Docker 照着它一层层搭出镜像。我刚开始写 Dockerfile 时,总觉得每个指令差不多,混着用也没事。直到有一次线上容器频繁重启,排查半天发现是 CMD 和 ENTRYPOINT 写反了…… 嗯,从那以后我再也不敢小看这几个指令了。
4.1 FROM —— 一切的基础
每个 Dockerfile 的第一行几乎都是 FROM。它指定了基础镜像,你在这个基础上加东西。
重点:基础镜像的版本一定要显式写,别偷懒用 latest。我在项目里遇到过因为基础镜像升级,导致原有依赖不兼容的事,后来全队统一要求写具体版本号。
FROM node:18-alpine # 好写法
FROM node:latest # 危险写法,下次构建可能就不是同一个镜像了
特殊场景下,可以用 FROM scratch 从零开始构建,比如做极简的 Golang 静态二进制镜像。但日常开发别这么干,除非你真的很清楚自己在做什么。
4.2 RUN —— 构建时执行命令
RUN 在构建阶段执行,每一条 RUN 都会创建新的镜像层。层数多了镜像就会臃肿,而且构建时很难命中缓存。
我习惯把同类操作合并成一条 RUN,比如安装系统包、清理缓存一起做:
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates && \
rm -rf /var/lib/apt/lists/*
技巧:尽量把可能变化的步骤放在 Dockerfile 后边,不变的放前边。这样能最大化利用层缓存,每次构建只重跑改动的几层。
你想想看,如果每次安装包都单独写一条 RUN,那就产生好几层。不光镜像大,构建也慢。
4.3 CMD 与 ENTRYPOINT —— 容器启动的舵手
这两个指令容易被搞混,但它们分工很明确。我画了张图帮你理清关系:
从图里能看出来:ENTRYPOINT 定义的是“程序本身”,CMD 定义的是“默认参数”。运行时你可以覆盖 CMD,但 ENTRYPOINT 通常固定。
| 特性 | ENTRYPOINT | CMD |
|---|---|---|
| 作用 | 设定容器的主进程 | 为主进程提供默认参数 |
| 运行时能否覆盖 | 需使用 --entrypoint 选项 | 命令行参数直接覆盖 |
| 推荐用法 | 固定入口程序(如 nginx、python app) | 提供可替换的默认参数(如 ["-c", "config.json"]) |
| 常见坑 | 写成 shell 形式不注意信号传递 | 和 ENTRYPOINT 同时使用时不传参导致报错 |
重要:如果同时写了 ENTRYPOINT 和 CMD,Docker 会把它们拼接起来执行。例如 ENTRYPOINT ["nginx"] + CMD ["-g", "daemon off;"],最终执行的是 nginx -g daemon off;。这很常用。
4.4 书写形式:exec 还是 shell?
每个指令都有两种写法:exec 形式(JSON 数组)和 shell 形式(普通字符串)。
# exec 形式(推荐)
RUN ["apt-get", "install", "-y", "curl"]
CMD ["python", "app.py"]
ENTRYPOINT ["nginx", "-g", "daemon off;"]
# shell 形式
RUN apt-get install -y curl
CMD python app.py
ENTRYPOINT nginx -g "daemon off;"
我个人习惯全部用 exec 形式。为什么?因为 shell 形式会启动 /bin/sh -c,当容器收到 SIGTERM 时,主进程可能无法正确响应,导致容器关不掉。尤其是做微服务健康检查时,这个问题很要命。
曾经有一次我用 shell 形式写 ENTRYPOINT,结果调用 docker stop 总得等 10 秒超时才能杀掉进程。换成 exec 形式后瞬间优雅关闭。
4.5 组合最佳实践
在实际项目中,我习惯这样组合:
- 固定入口并用默认配置:
ENTRYPOINT ["/usr/local/bin/myapp"]+CMD ["--config", "/etc/myapp/default.yml"]。用户运行时可以加其他参数覆盖。 - 可切换的入口: 如果只写
CMD,那用户很容易替换成 bash 调试。但注意这样入口不固定,适合开发镜像。 - 永远不要同时用 shell 形式的 ENTRYPOINT 和 CMD,拼接逻辑会很混乱。
# 一个标准的生产环境 Dockerfile 示例
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
ENTRYPOINT ["uvicorn"]
CMD ["main:app", "--host", "0.0.0.0", "--port", "8000"]
小贴士:如果你需要让容器默认启动一个 web 服务,但允许用户传参覆盖(比如指定端口),那用 ENTRYPOINT+CMD 的方式最灵活。
4.6 避坑清单
- RUN 里用
&&合并命令,避免产生过多层。 - 基础镜像标签别用
latest,用具体版本号。 - CMD 和 ENTRYPOINT 的数组语法要紧跟逗号,不能有多余空格。
- 如果用了 shell 形式,注意特殊字符转义,比如美元符号
$。
警惕:Dockerfile 里不要写敏感信息(密码、密钥)。镜像层是持久的,你删了那行秘密也在历史层里躺着。我有个同事曾经把数据库密码直接写在 RUN 里,镜像推到了仓库…… 嗯,后来全组花了一晚上重置所有容器。
弄懂这四个指令,你就掌握了写 Dockerfile 的核心。剩下的事情就是组合它们,搭出轻巧、安全、可维护的镜像。