2. Dockerfile 最佳实践:减少层数、合并 RUN 指令、合理使用 .dockerignore

在自动化流水线中,Docker 镜像的构建效率直接影响部署速度。Dockerfile 的编写质量决定了镜像层数、缓存命中率以及最终镜像大小。本章聚焦三个核心实践:减少层数、合并 RUN 指令、合理使用 .dockerignore,帮助你在流水线中获得更快的构建速度和更小的镜像体积。

2.1 镜像层数与构建缓存原理

Docker 镜像由只读层(Layer)堆叠而成,每一层对应 Dockerfile 中的一条指令。构建时,Docker 会逐条执行指令并生成新层。如果某条指令及其上下文未发生变化,Docker 会复用缓存层,跳过执行。

核心原则: 层数越少,镜像越小;缓存命中率越高,构建越快。但层数并非唯一指标——层的顺序和指令的稳定性同样关键。
Docker 镜像层与缓存命中机制 FROM ubuntu:22.04 RUN apt-get update COPY app.py /app/ RUN pip install -r req CMD ["python","app.py"] 缓存失效 app.py 变更 后续层全部 重新构建 FROM ubuntu:22.04 RUN apt-get update COPY requirements.txt RUN pip install -r req CMD ["python","app.py"] 缓存命中 requirements.txt 未变更 前 4 层全部复用

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 忽略规则对构建上下文的影响

.dockerignore 对构建上下文的影响 无 .dockerignore 发送全部文件 包含 .git/ (50MB) 包含 node_modules/ (200MB) 包含 __pycache__/ (30MB) 上下文大小: ~300MB 优化 有 .dockerignore 排除 .git/ 排除 node_modules/ 排除 __pycache__/ 排除日志文件 上下文大小: ~5MB 效果对比 构建速度提升 60% 缓存命中率提升 镜像体积减少 40%

2.5 综合实践清单

在自动化流水线中,将以下检查项集成到 CI/CD 流程中,确保每次构建都遵循最佳实践:

  1. 层数检查: 使用 docker history 或 dive 工具分析镜像层数,目标控制在 15 层以内。
  2. RUN 指令合并: 检查 Dockerfile 中是否有连续多个 RUN 指令,合并为逻辑分组。
  3. .dockerignore 验证: 确保 .dockerignore 文件存在,并排除所有非必要文件。
  4. 缓存顺序优化: 将变更频率低的指令(如安装系统依赖)放在前面,变更频率高的指令(如复制代码)放在后面。
  5. 多阶段构建: 对于编译型语言(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 的对比测试,观察构建时间差异,验证缓存策略是否生效。