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 都会生成一个新的镜像层。层数越少,镜像越小,构建速度越快。
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),这样既保持灵活性又避免误覆盖。
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"])。