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"]
小技巧:当你的命令里包含环境变量或管道时,用 shell 模式更方便。但如果你要避免 shell 的字符串解析带来的坑,exec 模式更安全。

4.3 CMD 与 ENTRYPOINT:容器启动时的行为

这两个指令容易混淆。我刚开始也搞不清。记住一句话:ENTRYPOINT 是跑不掉的入口,CMD 是默认的参数。你可以用 docker run 覆盖 CMD,但很难覆盖 ENTRYPOINT(除非用 --entrypoint 参数)。

看个对比表格:

指令作用能否被 docker run 覆盖典型用法
CMD提供默认命令或参数可以,在镜像名后直接写命令即可CMD ["node", "app.js"]
ENTRYPOINT设定容器的主进程,固定入口不能直接覆盖,必须用 --entrypointENTRYPOINT ["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。这样既保证了入口的可控性,又允许用户灵活传参。

我曾经犯过的错:在同一个 Dockerfile 里写了两个 ENTRYPOINT。后来才知道,只有最后一个生效。如果你想在 shell 脚本里做初始化,然后用 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 构建与容器启动时各指令的关系:

Dockerfile 指令关系图 Dockerfile 构建与容器启动流程 FROM 基础镜像 RUN 构建命令 更多 RUN 构建完成 ENTRYPOINT CMD (默认参数) 容器主进程启动 注意点 • CMD 和 ENTRYPOINT 都支持 JSON 数组形式(exec 式)和普通字符串形式(shell 式)。 • 只有最后一个 FROM 之前的 RUN 才会被真正执行(多阶段构建时)