4. Dockerfile 基础:FROM、RUN、CMD、ENTRYPOINT 指令
写 Dockerfile 就像搭积木。每一步都决定最终镜像长什么样。今天咱们就聊四个最基础的指令 —— FROM、RUN、CMD、ENTRYPOINT。我刚开始用 Docker 时,光这几个就踩了不少坑。说白了,理解了它们,你就能写出像样的 Dockerfile 了。
4.1 FROM:选好你的地基
FROM 是每条 Dockerfile 的第一行。它指定了基础镜像。没有 FROM,镜像就建不起来。我个人习惯用 Alpine 变体,体积小、安全更新快。但要注意:基础镜像的版本号一定要显式写,别用 latest。为什么?因为 latest 会随着上游更新而变化,导致你明天构建的镜像和今天的不一样,调试起来非常头疼。
# 推荐写法
FROM node:20-alpine
# 不推荐
FROM node:latest
我在项目中遇到过一件事:某天 CI 突然挂了,发现是 node:latest 从 LTS 变成了 Current 版本,一些 API 不兼容。从那以后,我要求团队所有 Dockerfile 都用精确的版本标签。
4.2 RUN:执行构建命令
RUN 是在镜像构建过程中执行的命令。每一个 RUN 都会产生一个新层。层多了,镜像就大。所以我的习惯是:把相关的命令合并到一条 RUN 里,用 && 连接,最后再清理临时文件。
# 不合并,会多出两层
RUN apt-get update
RUN apt-get install -y wget
RUN rm -rf /var/lib/apt/lists/*
# 合并写法,只生成一层
RUN apt-get update && \
apt-get install -y wget && \
rm -rf /var/lib/apt/lists/*
再补充一点:RUN 分两种模式 —— shell 模式和 exec 式。默认写命令不加数组就是 shell 模式,调用 /bin/sh -c。如果你想绕过 shell,用 JSON 数组形式。
# shell 模式
RUN echo "Hello"
# exec 模式(推荐用在变量少的地方)
RUN ["echo", "Hello"]
4.3 CMD 与 ENTRYPOINT:容器启动时的行为
这两个指令容易混淆。我刚开始也搞不清。记住一句话:ENTRYPOINT 是跑不掉的入口,CMD 是默认的参数。你可以用 docker run 覆盖 CMD,但很难覆盖 ENTRYPOINT(除非用 --entrypoint 参数)。
看个对比表格:
| 指令 | 作用 | 能否被 docker run 覆盖 | 典型用法 |
|---|---|---|---|
| CMD | 提供默认命令或参数 | 可以,在镜像名后直接写命令即可 | CMD ["node", "app.js"] |
| ENTRYPOINT | 设定容器的主进程,固定入口 | 不能直接覆盖,必须用 --entrypoint | ENTRYPOINT ["docker-entrypoint.sh"] |
实战中最常见的搭配是:ENTRYPOINT 固定启动脚本,CMD 提供默认参数。比如官方 Redis 镜像就是这样做的:
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["redis-server"]
用户跑 docker run redis --appendonly yes 时,实际执行的是 docker-entrypoint.sh redis-server --appendonly yes。这样既保证了入口的可控性,又允许用户灵活传参。
exec "$@" 启动主进程 —— 对,官方就是这么干的。4.4 实战:一个完整的 Node.js Dockerfile
把上面三个指令串起来,就是一份可用的 Dockerfile:
# 基础镜像
FROM node:20-alpine
# 设置工作目录
WORKDIR /app
# 先复制 package.json 和锁文件,利用 Docker 缓存
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# 再复制源码(改动频率高,放后面)
COPY . .
# 声明运行时端口
EXPOSE 3000
# 固定入口为 node,默认启动 app.js
ENTRYPOINT ["node"]
CMD ["app.js"]
注意这里的顺序:把依赖安装放在 COPY 源码之前,是为了让 package.json 不变时,可以直接复用之前的层,加速构建。
- FROM 选稳定版本,别用 latest。
- RUN 合并命令,减少层数。
- CMD 可被覆盖,ENTRYPOINT 是固定入口。
- 两者结合使用,让镜像更灵活。
下面这张图可以帮你理清 Dockerfile 构建与容器启动时各指令的关系: