3. Dockerfile 编写规范:FROM、RUN、CMD、ENTRYPOINT、COPY、ADD、WORKDIR 指令详解

Dockerfile 是构建容器镜像的“配方”,其指令的编写质量直接决定了镜像的安全性、构建速度与运行稳定性。本章聚焦于 7 个核心指令,通过规范用法、常见误区与对比表格,帮助你写出生产级 Dockerfile。

3.1 指令概览与执行阶段

Dockerfile 中的指令分为构建阶段与运行阶段两类:

  • 构建阶段指令:FROM、RUN、COPY、ADD、WORKDIR —— 在 docker build 时执行,影响镜像层。
  • 运行阶段指令:CMD、ENTRYPOINT —— 在 docker run 时执行,定义容器启动行为。
💡 核心原则:每个 RUN、COPY、ADD 都会生成一个新的镜像层。层数越少,镜像越小,构建速度越快。
Dockerfile 指令执行阶段与镜像层关系 Dockerfile 指令执行阶段与镜像层关系 构建阶段(docker build) FROM base:tag RUN apt-get install COPY / ADD 运行阶段(docker run) ENTRYPOINT ["executable"] CMD ["param1", "param2"] 镜像层固化 每个 RUN / COPY / ADD 指令生成一个只读层,CMD / ENTRYPOINT 不产生新层

3.2 指令详解与规范

3.2.1 FROM —— 基础镜像选择

FROM 必须是 Dockerfile 的第一条指令,定义构建所依赖的基础镜像。选择原则:

  • 首选官方镜像:如 python:3.11-slim、node:20-alpine,安全且体积小。
  • 指定精确版本:避免使用 latest 标签,防止构建环境漂移。
  • 多阶段构建:使用多个 FROM 分离构建环境与运行环境,显著减小最终镜像体积。
✅ 推荐写法:FROM python:3.11-slim AS builder 和 FROM python:3.11-slim AS runtime

3.2.2 RUN —— 构建时命令执行

在构建过程中执行命令,常用于安装依赖、编译代码、设置权限。规范要点:

  • 合并命令:使用 && 连接多个命令,减少层数。
  • 清理缓存:在同一个 RUN 中删除临时文件,避免缓存残留增大镜像。
  • 避免交互:使用 DEBIAN_FRONTEND=noninteractive 等环境变量跳过交互提示。
# ❌ 不推荐:多个 RUN 产生多层
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*

# ✅ 推荐:合并为单层
RUN apt-get update && \
    apt-get install -y curl --no-install-recommends && \
    rm -rf /var/lib/apt/lists/*

3.2.3 COPY 与 ADD —— 文件复制

特性 COPY ADD
基本功能 从构建上下文复制文件到镜像 同 COPY,额外支持 URL 下载与自动解压
自动解压 不支持 支持 tar.gz / zip 等压缩包自动解压
URL 支持 不支持 支持从远程 URL 下载文件
推荐场景 所有本地文件复制(更透明、可预测) 仅当需要自动解压压缩包时使用
⚠️ 避坑指南:ADD 的 URL 下载行为不可缓存,且自动解压可能引入意外文件。生产环境优先使用 COPY,仅在需要解压本地 tar 包时使用 ADD。

3.2.4 WORKDIR —— 工作目录设置

WORKDIR 设置后续指令的工作目录,如果目录不存在则自动创建。规范用法:

  • 使用绝对路径:如 WORKDIR /app,避免相对路径带来的歧义。
  • 放在 COPY 之前:先设置 WORKDIR,再 COPY 文件,确保文件被复制到预期目录。
  • 每个 RUN / CMD / ENTRYPOINT 前可重复设置:但建议在文件头部统一设置一次。
# ✅ 推荐结构
FROM node:20-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
CMD ["node", "server.js"]

3.2.5 CMD 与 ENTRYPOINT —— 容器启动行为

两者共同定义容器启动时的默认命令,但角色不同:

指令 作用 是否可被覆盖 典型用法
ENTRYPOINT 定义容器的主执行程序 使用 --entrypoint 覆盖 ENTRYPOINT ["python"]
CMD 提供 ENTRYPOINT 的默认参数,或直接定义命令 运行时可被 docker run 后的参数覆盖 CMD ["app.py"]

组合规则:

  • ENTRYPOINT + CMD:CMD 作为默认参数传递给 ENTRYPOINT。例如 ENTRYPOINT ["nginx"] + CMD ["-g", "daemon off;"],运行时可通过 docker run nginx -c /custom.conf 覆盖 CMD 部分。
  • 仅 CMD:直接定义启动命令,运行时完全可被替换。
  • 仅 ENTRYPOINT:固定执行程序,不接受额外参数。
📌 生产建议:将 ENTRYPOINT 设为固定可执行文件(如 java、python),CMD 设为默认参数(如 -jar app.jar),这样既保持灵活性又避免误覆盖。
CMD 与 ENTRYPOINT 组合行为决策图 CMD 与 ENTRYPOINT 组合行为决策图 组合 1:仅 CMD CMD ["node", "app.js"] docker run 可完全替换 组合 2:仅 ENTRYPOINT ENTRYPOINT ["nginx"] 固定执行,不接受参数 组合 3:ENTRYPOINT + CMD ENTRYPOINT ["python"] CMD ["app.py"] CMD 作为默认参数 推荐使用组合 3:ENTRYPOINT 固定主程序,CMD 提供可覆盖的默认参数

3.3 完整规范示例

以下是一个符合生产规范的 Dockerfile 示例,整合了上述所有指令的最佳实践:

# 1. 多阶段构建:构建阶段
FROM golang:1.21-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .

# 2. 运行阶段:最小化镜像
FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /build/myapp .
EXPOSE 8080
ENTRYPOINT ["/app/myapp"]
CMD ["--config", "/app/config.yaml"]
✅ 规范检查清单:
  • FROM 使用精确版本标签,非 latest
  • RUN 命令合并,且清理缓存
  • COPY 优先于 ADD,仅解压场景用 ADD
  • WORKDIR 使用绝对路径,放在 COPY 之前
  • ENTRYPOINT + CMD 组合,明确主程序与默认参数

3.4 常见误区与避坑

误区 问题 正确做法
使用 ADD . /app 复制整个目录 可能引入 .dockerignore 未排除的敏感文件 使用 COPY . /app 并配合 .dockerignore
在 RUN 中使用 cd 切换目录 仅对当前 RUN 生效,后续指令回到原目录 使用 WORKDIR 持久化工作目录
CMD 使用 shell 格式 CMD node app.js PID 1 问题,信号无法正确传递 使用 exec 格式 CMD ["node", "app.js"]
多个 FROM 未使用 AS 命名 多阶段构建无法引用前一阶段 使用 FROM ... AS builder 命名阶段
⚠️ 关键提醒:CMD 和 ENTRYPOINT 的 shell 格式(CMD node app.js)会启动一个 /bin/sh -c 子进程,导致容器无法直接接收 SIGTERM 信号。生产环境务必使用 exec 格式(CMD ["node", "app.js"])。