多阶段构建(Multi-stage Build)
问题的起源
构建应用通常需要大量工具:编译器、构建系统、测试框架、开发依赖……但运行应用只需要最终产物。如果把所有工具都打包进镜像,镜像体积会非常大。
示例一:Go 应用多阶段构建
# === 构建阶段:使用完整 Go 工具链 ===
FROM golang:1.21-alpine AS builder
WORKDIR /build
# 先复制 go.mod 和 go.sum,利用层缓存
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 生成静态二进制,GOOS=linux 确保跨平台兼容
RUN CGO_ENABLED=0 GOOS=linux go build -a -o server ./cmd/server
# === 运行阶段:使用空镜像 ===
FROM scratch
# 如需 HTTPS,需要 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# 只复制编译好的二进制文件
COPY --from=builder /build/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
示例二:React 前端多阶段构建
# === 构建阶段 ===
FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# === 生产运行阶段:Nginx 静态服务 ===
FROM nginx:1.25-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
基础镜像选择策略
基础镜像的选择直接影响镜像大小、安全性和维护成本:
| 镜像类型 | 代表 | 大小 | 优点 | 缺点 |
|---|---|---|---|---|
| 完整版 | ubuntu:22.04 |
~77MB | 工具完整,调试方便 | 大,攻击面大 |
| Slim 版 | node:18-slim |
~60MB | 比完整版小,基于 Debian | 缺少部分工具 |
| Alpine | node:18-alpine |
~5MB | 极小,安全 | musl libc 兼容性问题 |
| Distroless | gcr.io/distroless/nodejs |
~30MB | 无 shell、无包管理器,最安全 | 调试极难 |
Alpine 的兼容性问题 — Alpine 使用 musl libc 而非 glibc,某些原生模块(如 canvas、bcrypt 等 Node.js 模块)可能无法在 Alpine 上运行,或需要额外安装 python3、make、g++ 重新编译。
镜像瘦身技巧全览
技巧一:RUN 命令合并
# ❌ 每条 RUN 创建一层,多余的缓存文件被分散保存
RUN apt-get update
RUN apt-get install -y curl git
RUN rm -rf /var/lib/apt/lists/* # 这层根本无法删除前面层的文件!
# ✅ 合并为一层,清理在同一层完成
RUN apt-get update \
&& apt-get install -y curl git \
&& rm -rf /var/lib/apt/lists/*
技巧二:合理使用 .dockerignore
确保以下目录/文件不进入构建上下文(参见第3章示例)。
技巧三:选择合适基础镜像
优先选择 Alpine 或 Slim 版本,或使用多阶段构建的 scratch/distroless 作为最终运行镜像。
技巧四:多阶段构建
将构建工具链完全排除在最终镜像之外(参见上文示例)。
技巧五:不安装不必要的包
# 只安装生产依赖
RUN npm ci --only=production
# apt 不安装推荐包
RUN apt-get install -y --no-install-recommends curl
镜像标签策略
latest 标签的陷阱 — latest 只是一个普通标签,不代表"最新版"——除非你主动推送时打了这个标签。在生产环境使用 latest 意味着每次 docker pull 可能拉取不同版本,导致不可预期的行为。
推荐的标签策略:
# 语义化版本标签(推荐生产使用)
docker build -t myapp:1.2.3 .
docker build -t myapp:1.2 .
docker build -t myapp:1 .
# Git Commit SHA(完全不可变,适合 CI/CD 溯源)
docker build -t myapp:$(git rev-parse --short HEAD) .
# 可同时打多个标签
docker build \
-t myapp:1.2.3 \
-t myapp:latest \
.
| 标签类型 | 示例 | 适用场景 |
|---|---|---|
| 语义化版本 | myapp:1.2.3 | 生产部署,可回滚 |
| Git SHA | myapp:a3f9d2c | CI/CD 追踪,不可变 |
| 环境标签 | myapp:staging | 环境区分 |
| 分支标签 | myapp:feature-login | 开发测试 |
| latest | myapp:latest | 仅用于开发/演示 |
镜像安全扫描
Docker Scout
# 扫描本地镜像的 CVE 漏洞
docker scout cves myapp:1.0
# 快速概览(摘要)
docker scout quickview myapp:1.0
# 与基础镜像对比
docker scout compare myapp:1.1 --to myapp:1.0
Trivy(Aqua Security 开源)
# 安装 Trivy
brew install trivy
# 扫描本地镜像
trivy image myapp:1.0
# 只显示高危和严重漏洞
trivy image --severity HIGH,CRITICAL myapp:1.0
docker build 命令参数详解
docker build \
--tag myapp:1.0 \ # 镜像名和标签(简写 -t)
--build-arg VERSION=1.0 \ # 传入 ARG 变量
--target builder \ # 多阶段构建:只构建到 builder 阶段
--platform linux/amd64,linux/arm64 \ # 跨平台构建(需要 buildx)
--no-cache \ # 禁用缓存,强制全新构建
--pull \ # 总是拉取最新基础镜像
--file ./infra/Dockerfile \ # 指定 Dockerfile 路径(默认 ./Dockerfile)
. # 构建上下文目录
跨平台构建(多架构镜像)
# 创建并使用 buildx builder(支持多平台)
docker buildx create --use --name multi-builder
docker buildx inspect --bootstrap
# 构建并推送 amd64 和 arm64 镜像
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:1.0 \
--push \
.
BuildKit 缓存挂载(Cache Mounts)
BuildKit(Docker 27 中默认启用)的缓存挂载是构建加速的利器。它允许 RUN 指令在构建之间共享缓存目录,而不会增加镜像体积。
- BuildKit Docker 新一代构建引擎,相比旧版 builder 支持:并行构建阶段、缓存挂载、Secret 安全传递、更好的缓存利用率。Docker 18.09+ 可用,Docker 23+ 默认启用。
- --mount=type=cache BuildKit 专属特性,让 RUN 指令可以读写一个持久的缓存目录,该目录在构建结束后不会保留在镜像层中,但会在下次构建时仍然存在,大幅加速依赖安装。
# Node.js:缓存 npm 下载的包
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \ # npm 缓存目录
npm ci # 从缓存安装,无需重复下载
COPY . .
RUN npm run build
# Go:缓存 go module 下载
FROM golang:1.23-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/root/go/pkg/mod \ # Go module 缓存
--mount=type=cache,target=/root/.cache/go-build \ # Go 编译缓存
go mod download
COPY . .
RUN --mount=type=cache,target=/root/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o server ./cmd/server
# Python:缓存 pip 下载的包
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \ # pip 缓存目录
pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]
# apt:缓存 apt 已下载的包
FROM ubuntu:24.04
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y \
curl git vim \
# 注意:不需要 rm -rf /var/lib/apt/lists/*
# 因为缓存不进入镜像层,apt 缓存自然不占用镜像空间
缓存挂载 vs 普通层缓存的区别 — 普通层缓存在 CI 环境中经常失效(每次新机器或每次强制构建都重新安装)。缓存挂载的缓存存储在 BuildKit 的缓存目录中,只要在同一台机器上构建,就会持续有效,首次安装 1000 个包需要 60 秒,后续只需 2 秒。
BuildKit 的其他高级特性
# 绑定挂载(--mount=type=bind):临时挂载不复制到镜像
RUN --mount=type=bind,source=scripts/,target=/scripts \
sh /scripts/setup.sh # scripts/ 目录只在构建时可见,不进入镜像
# SSH 挂载:安全使用 SSH 密钥访问私有仓库
RUN --mount=type=ssh \
git clone git@github.com:private/repo.git
# 构建时:docker build --ssh default .
BuildKit 需要在 Dockerfile 顶部声明 — 使用 BuildKit 特性(--mount、--secret)时,如果你的 Docker 版本较旧,需要在 Dockerfile 第一行添加 # syntax=docker/dockerfile:1 并在构建前设置 DOCKER_BUILDKIT=1。Docker 23+ 默认启用 BuildKit,Docker 27 无需额外配置。
本章小结 — 多阶段构建是现代 Docker 最佳实践中最重要的技术,可将镜像从 GB 级缩减到 MB 级。BuildKit 缓存挂载解决了 CI/CD 中依赖重复安装的性能问题,是 Docker 27 的核心优化特性。合理的标签策略(语义化版本 + Git SHA)确保生产部署的可追溯性和稳定性。