多阶段构建(Multi-stage Build)
问题的起源
构建应用通常需要大量工具:编译器、构建系统、测试框架、开发依赖……但运行应用只需要最终产物。如果把所有工具都打包进镜像,镜像体积会非常大。
单阶段构建(Go 应用)
FROM golang:1.21(800MB)+ 源码 + 编译 = 最终镜像 850MB。运行时根本用不到 Go 编译工具链。
多阶段构建(Go 应用)
阶段1:FROM golang:1.21 编译得到二进制文件。阶段2:FROM scratch 只复制二进制文件 = 最终镜像 10MB。
示例一: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 \
.
本章小结 — 多阶段构建是现代 Docker 最佳实践中最重要的技术,可将镜像从 GB 级缩减到 MB 级。合理的标签策略确保生产部署的可追溯性和稳定性。