2. Dockerfile 最佳实践:减少层数、合并 RUN 指令、合理使用 .dockerignore
在自动化流水线中,Docker 镜像的构建效率直接影响部署速度。Dockerfile 的编写质量决定了镜像层数、缓存命中率以及最终镜像大小。本章聚焦三个核心实践:减少层数、合并 RUN 指令、合理使用 .dockerignore,帮助你在流水线中获得更快的构建速度和更小的镜像体积。
2.1 镜像层数与构建缓存原理
Docker 镜像由只读层(Layer)堆叠而成,每一层对应 Dockerfile 中的一条指令。构建时,Docker 会逐条执行指令并生成新层。如果某条指令及其上下文未发生变化,Docker 会复用缓存层,跳过执行。
核心原则: 层数越少,镜像越小;缓存命中率越高,构建越快。但层数并非唯一指标——层的顺序和指令的稳定性同样关键。
2.2 减少层数的具体策略
每一层都会增加镜像体积和构建时间。减少层数最直接的方法是合并指令和清理中间产物。
| 策略 | 说明 | 示例 |
|---|---|---|
| 合并 RUN 指令 | 将多个 RUN 用 && 连接,减少层数 |
RUN apt-get update && apt-get install -y curl |
| 清理包管理器缓存 | 在同一个 RUN 中删除缓存文件,避免残留 | RUN apt-get clean && rm -rf /var/lib/apt/lists/* |
| 使用多阶段构建 | 构建阶段与运行阶段分离,只保留最终产物 | FROM golang:1.20 AS builder → FROM alpine:3.18 |
| 避免不必要的 COPY | 只复制构建所需的最小文件集 | COPY --from=builder /app/bin /app/ |
最佳实践: 将
apt-get update 和 apt-get install 放在同一个 RUN 中,并在末尾清理缓存。这样既减少层数,又避免缓存过期导致的安全问题。
2.3 合并 RUN 指令的正确姿势
合并 RUN 指令不是简单地将所有命令写在一行,而是需要遵循逻辑分组和缓存友好原则。
2.3.1 错误示范:过度拆分
# 不推荐:产生 4 层,且中间层包含临时文件
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y vim
RUN apt-get clean
2.3.2 正确示范:逻辑合并
# 推荐:1 层,包含安装和清理
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
vim \
ca-certificates && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
2.3.3 缓存友好型合并
将稳定层(如安装系统依赖)放在前面,易变层(如复制代码)放在后面,最大化缓存命中。
# 缓存友好:先安装依赖,再复制代码
FROM python:3.11-slim
# 稳定层:系统依赖 + Python 包
RUN apt-get update && \
apt-get install -y --no-install-recommends \
gcc \
libpq-dev && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# 稳定层:Python 依赖(requirements.txt 变更频率低)
COPY requirements.txt /app/
RUN pip install --no-cache-dir -r /app/requirements.txt
# 易变层:应用代码(频繁变更)
COPY . /app/
CMD ["python", "/app/main.py"]
注意: 合并 RUN 指令时,如果某条命令失败,整个 RUN 层都会失败。建议在命令末尾添加
|| true 或使用 set -e 确保脚本行为可预测。
2.4 合理使用 .dockerignore
.dockerignore 文件用于排除构建上下文中不必要的文件,减少发送到 Docker daemon 的数据量,从而加速构建并避免缓存污染。
2.4.1 典型 .dockerignore 示例
# 版本控制
.git/
.gitignore
.gitattributes
# 开发环境
node_modules/
vendor/
__pycache__/
*.pyc
.venv/
.env
# 构建产物
dist/
build/
*.tar.gz
*.zip
# IDE 配置
.idea/
.vscode/
*.swp
# 日志
*.log
npm-debug.log*
# Docker 相关
Dockerfile
.dockerignore
docker-compose*.yml
2.4.2 忽略规则优先级
| 规则类型 | 示例 | 说明 |
|---|---|---|
| 路径 | /data |
忽略根目录下的 data 文件夹 |
| 通配符 | *.log |
忽略所有 .log 文件 |
| 否定规则 | !important.log |
保留 important.log(即使 *.log 被忽略) |
| 目录递归 | node_modules/ |
忽略所有 node_modules 目录 |
2.4.3 忽略规则对构建上下文的影响
2.5 综合实践清单
在自动化流水线中,将以下检查项集成到 CI/CD 流程中,确保每次构建都遵循最佳实践:
- 层数检查: 使用
docker history或dive工具分析镜像层数,目标控制在 15 层以内。 - RUN 指令合并: 检查 Dockerfile 中是否有连续多个 RUN 指令,合并为逻辑分组。
- .dockerignore 验证: 确保 .dockerignore 文件存在,并排除所有非必要文件。
- 缓存顺序优化: 将变更频率低的指令(如安装系统依赖)放在前面,变更频率高的指令(如复制代码)放在后面。
- 多阶段构建: 对于编译型语言(Go、Java、Rust),使用多阶段构建分离构建环境和运行环境。
流水线集成建议: 在 CI/CD 脚本中添加
docker build --check 命令(Docker 23.0+),自动检查 Dockerfile 中的常见问题,包括层数过多、缓存顺序不当等。
2.6 常见误区与避坑
| 误区 | 错误做法 | 正确做法 |
|---|---|---|
| 过度合并 | 将所有命令写在一行,导致可读性差 | 按逻辑分组,使用 \ 换行 |
| 忽略缓存顺序 | 先 COPY 代码,再安装依赖 | 先 COPY 依赖文件,安装后再 COPY 代码 |
| .dockerignore 缺失 | 不创建 .dockerignore,发送整个项目目录 | 创建 .dockerignore,排除 .git、node_modules 等 |
| 忽略清理 | 安装包后不清理缓存 | 在同一个 RUN 中清理 apt/yum/pip 缓存 |
快速自查: 在流水线中加入
docker build --no-cache 和 docker build --cache-from 的对比测试,观察构建时间差异,验证缓存策略是否生效。