Docker: 镜像优化与最佳实践
最后更新:2026-08-26
优化镜像不只是减小体积——更小的镜像意味着更快的部署、更低的存储成本和更小的攻击面。
1. 你将学到
- 镜像层数优化策略
- RUN 指令合并技巧
- 依赖清理与缓存管理
- Dockerfile Lint 工具使用
- 构建缓存优化方法
2. 一个 CI 工程师的真实故事
(1) 痛点:CI 构建时间从 8 分钟飙升到 25 分钟
Bob 的 CI 构建时间突然从 8 分钟飙升到 25 分钟。他排查发现,每次构建都重新安装所有依赖——因为 COPY . . 放在 RUN pip install 前面,任何代码变更都导致依赖安装层缓存失效。20 分钟的等待让开发效率大幅下降。
(2) 缓存优化的解法
调整 Dockerfile 指令顺序,把不常变的 COPY package.json 提前,利用层缓存把构建时间降回 3 分钟。
DOCKERFILE
# Before: code change invalidates pip cache
COPY . .
RUN pip install -r requirements.txt
# After: dependency change rate << code change rate
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
(3) 收益:构建时间 25 分钟→3 分钟
只调整了两行代码顺序,构建时间从 25 分钟降到 3 分钟(缓存命中时),CI 管道效率提升 8 倍。
3. 镜像优化核心策略
(1) 优化前后对比框架
graph TB
subgraph Before["Before Optimization"]
B1["FROM ubuntu:22.04<br/>77 MB"] --> B2["RUN apt-get install<br/>300 MB"]
B2 --> B3["COPY . .<br/>含 node_modules"]
B3 --> B4["RUN npm install<br/>200 MB"]
B4 --> B5["Final: 1.2 GB"]
end
subgraph After["After Optimization"]
A1["FROM node:20-alpine<br/>135 MB"] --> A2["COPY package.json"]
A2 --> A3["RUN npm ci<br/>50 MB"]
A3 --> A4["COPY . .<br/>无 node_modules"]
A4 --> A5["Final: 180 MB"]
end
(2) 优化策略总览
| 策略 | 效果 | 实施难度 |
|---|---|---|
| 更换基础镜像(alpine/slim) | 体积↓50-80% | ⭐ |
| 多阶段构建 | 体积↓60-90% | ⭐⭐ |
| RUN 合并 + 清理缓存 | 体积↓20-40% | ⭐ |
| 调整 COPY 顺序 | 构建速度↑3-8x | ⭐ |
| .dockerignore | 上下文↓30-90% | ⭐ |
| hadolint 检查 | 发现潜在问题 | ⭐⭐ |
4. RUN 指令合并
(1) 合并原则
每条 RUN 指令创建一个新层。合并相关操作到一个 RUN 中,并在同层内清理临时文件。
▶ 示例:指令合并(难度⭐⭐)
DOCKERFILE
# Bad: 3 separate layers, apt cache stays in image
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y nginx
# Good: 1 layer, cache cleaned within the same layer
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl \
nginx && \
rm -rf /var/lib/apt/lists/*
(2) --no-install-recommends 的效果
| 选项 | 安装内容 | 典型体积节省 |
|---|---|---|
| 默认 | 主包 + 推荐包 + 建议包 | 基线 |
--no-install-recommends |
仅主包 + 依赖 | ↓30-50% |
5. 缓存命中率优化
(1) 层缓存机制
Docker 构建镜像时,如果某层的指令和上下文未变,就复用缓存。一旦某层缓存失效,后续所有层都必须重建。
graph TB
L1["FROM python:3.12<br/>✅ Cache Hit"] --> L2["WORKDIR /app<br/>✅ Cache Hit"]
L2 --> L3["COPY requirements.txt<br/>✅ Cache Hit (no change)"]
L3 --> L4["RUN pip install<br/>✅ Cache Hit"]
L4 --> L5["COPY . .<br/>❌ Cache Miss (code changed)"]
L5 --> L6["CMD python app.py<br/>❌ Rebuild"]
▶ 示例:--no-cache 对比构建(难度⭐⭐)
BASH
# Normal build: uses cache where possible
docker build -t myapp:1.0 .
# Force rebuild without any cache
docker build --no-cache -t myapp:1.0 .
# Use a previous image as cache source (CI optimization)
docker build --cache-from=myapp:latest -t myapp:1.0 .
(2) 优化 Dockerfile 指令顺序
DOCKERFILE
# ============================================
# Optimized Dockerfile: high cache hit rate
# ============================================
FROM node:20-alpine
WORKDIR /app
# 1. Least frequently changed: package metadata
COPY package*.json ./
# 2. Dependency installation (cached unless package.json changes)
RUN npm ci --only=production
# 3. Most frequently changed: application code
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
| 变更场景 | 优化版 | 未优化版 |
|---|---|---|
| 只改代码 | ✅ npm ci 用缓存(5 秒) | ❌ npm ci 重建(3 分钟) |
| 改依赖版本 | ✅ npm ci 重建(必要) | ❌ npm ci 重建(同上) |
| 改 Dockerfile | ✅ 只重建受影响的层 | ❌ 全部重建 |
6. 镜像分析工具
▶ 示例:用 dive 分析镜像层(难度⭐⭐)
dive 是一个交互式镜像层分析工具,显示每一层添加/修改/删除了哪些文件。
BASH
# Install dive (Linux)
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo dpkg -i dive_0.12.0_linux_amd64.deb
# Analyze an image
dive nginx:latest
💡 提示: dive 的界面显示:① 左侧每层的大小和效率评分;② 右侧每层添加的文件树。红色标记浪费空间(被后续层覆盖/删除的文件)。
▶ 示例:用 hadolint 检查 Dockerfile(难度⭐⭐)
hadolint 是 Dockerfile 的 Lint 工具,检查最佳实践违规。
BASH
# Install hadolint (Linux)
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64
chmod +x /usr/local/bin/hadolint
# Check a Dockerfile
hadolint Dockerfile
💻 输出:
TEXT
📖 仅展示
Dockerfile:3 DL3013 warning: Pin versions in pip. Instead of `pip install <package>` use `pip install <package>==<version>`
Dockerfile:5 DL3059 info: Do not use `--no-cache-dir` in pip install. It is redundant when `--cache-from` is used.
Dockerfile:7 DL3008 warning: Pin versions in apt-get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
7. 优化前后 Dockerfile 完整对比
(1) Node.js 应用优化示例
DOCKERFILE
# ============================================
# BEFORE: Unoptimized Dockerfile (1.2 GB)
# ============================================
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["npm", "start"]
DOCKERFILE
# ============================================
# AFTER: Optimized Dockerfile (180 MB)
# ============================================
# 1. Alpine base instead of full Node
FROM node:20-alpine
WORKDIR /app
# 2. Copy dependency metadata first (cache optimization)
COPY package*.json ./
# 3. Production-only dependencies
RUN npm ci --only=production && \
npm cache clean --force
# 4. Copy source code (most frequently changed)
COPY . .
# 5. Run as non-root user
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]
(2) 优化效果对比
| 指标 | 优化前 | 优化后 | 改善 |
|---|---|---|---|
| 镜像体积 | 1.2 GB | 180 MB | 6.7x ↓ |
| 构建时间(代码变更) | 3 分钟 | 5 秒 | 36x ↓ |
| 构建时间(依赖变更) | 3 分钟 | 45 秒 | 4x ↓ |
| 安全漏洞 | 200+ | 30 | 6.7x ↓ |
| CIS Benchmark 评分 | 45 | 85 | +40 |
8. 各语言依赖安装策略
| 语言 | 依赖文件 | 安装命令 | 清理命令 |
|---|---|---|---|
| Node.js | package*.json |
npm ci --only=production |
npm cache clean --force |
| Python | requirements.txt |
pip install --no-cache-dir |
(内置 --no-cache-dir) |
| Go | go.mod go.sum |
go mod download |
go clean -modcache |
| Java | pom.xml / build.gradle |
mvn dependency:resolve |
清理 .m2 缓存 |
| Ruby | Gemfile Gemfile.lock |
bundle install --without dev |
清理 .bundle 缓存 |
9. 完整示例:Node.js 应用全流程优化
DOCKERFILE
# ============================================
# Fully optimized Node.js production Dockerfile
# Features: alpine, multi-stage, cache, non-root
# ============================================
# ---------- Stage 1: Build ----------
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ---------- Stage 2: Production ----------
FROM node:20-alpine
WORKDIR /app
# Copy only production dependencies
COPY package*.json ./
RUN npm ci --only=production && \
npm cache clean --force
# Copy built application from builder
COPY --from=builder /app/dist ./dist
# Security: non-root user
RUN addgroup -S appgroup && \
adduser -S appuser -G appgroup && \
chown -R appuser:appgroup /app
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
BASH
# Build and verify
docker build -t node-app:optimized .
# Compare sizes
docker images node-app
# Analyze with dive
dive node-app:optimized
# Check with hadolint
hadolint Dockerfile
❓ 常见问题
Q 每 RUN 一层都会增加镜像大小吗?
A 是的,即使 RUN 只是删除文件——因为新层记录"删除操作",而底层仍然存在。所以"安装+清理"必须在同一个 RUN 中完成,这样安装的临时文件和删除操作在同一个层里,底层看不到临时文件。
Q
apt-get clean 为什么能减镜像体积?A 单独的
apt-get clean 几乎无效(和 rm 同理)。必须在同一个 RUN 中:apt-get update && apt-get install && rm -rf /var/lib/apt/lists/*。--no-install-recommends 比 apt-get clean 效果更大。Q 怎么确认缓存是否被命中?
A
docker build 输出中:Using cache 表示命中,Step 3/7 : RUN xxx 表示重建。也可以用 --progress=plain 查看详细日志。CI 中 docker build --cache-from=app:latest 可复用上次构建的缓存。Q
--no-cache 什么时候用?A 两种场景:① 基础镜像有安全更新需要重新构建;② 构建结果异常怀疑缓存损坏。日常开发不要用
--no-cache,会让每次构建都从零开始。Q 如何检测镜像里的安全漏洞?
A
docker scout quickview <image>(Docker 官方)或 trivy image <image>(开源)。Trivy 是最流行的开源镜像扫描工具,可检测 OS 包和语言依赖的已知 CVE。建议在 CI 管道中集成扫描步骤。📖 小节
- 镜像优化核心:减少层数 + 同层清理 + 选择小基镜像
- RUN 合并原则:相关操作合并为一个 RUN,同层内安装+清理
- 缓存优化:不常变的指令放前面(COPY 依赖文件),常变的放后面(COPY 源码)
--no-install-recommends可减少 30-50% 的安装体积- dive 分析镜像层效率,hadolint 检查 Dockerfile 最佳实践
- 完整优化流程:alpine 基镜像 → 多阶段构建 → 缓存优化 → 非 root → 健康检查
📝 作业
- 基础题(难度⭐):用 dive 分析一个已有镜像(如 nginx:latest)的每一层,记录哪一层最大、添加了什么文件。
- 进阶题(难度⭐⭐):用 hadolint 检查第 7-9 课的 Dockerfile,修复所有 warning 级别的建议。
- 挑战题(难度⭐⭐⭐):对比 alpine/debian/slim 三种基镜像构建同一个 Python 应用的镜像大小差异,分析体积差异的原因。