Docker: 多阶段构建
最后更新:2026-08-26
一个镜像从 1.2 GB 降到 12 MB——多阶段构建是镜像瘦身最有效的技术。
1. 你将学到
- 多阶段构建的原理与优势
- 从构建阶段复制产物
- 选择最优运行基镜像
- 减小镜像体积的策略
- 缓存优化与调试技巧
2. 一个 Go 开发者的真实故事
(1) 痛点:1.2 GB 的应用镜像
Alice 的 Go 应用镜像有 1.2 GB——因为她用了 golang:1.22 作为基础镜像,里面包含完整的 Go 编译器、源码和工具链。但生产环境只需要一个 12 MB 的编译后的二进制文件。部署一次要等 5 分钟下载镜像,50 MB 带宽的云服务器每次发布都卡。
(2) 多阶段构建的解法
Bob 教她用多阶段构建:"第一阶段用 golang:1.22 compile,第二阶段从 alpine 复制编译产物。"
DOCKERFILE
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o server
# Stage 2: Run (only the binary, no Go compiler)
FROM alpine:3.19
COPY --from=builder /src/server /usr/local/bin/
CMD ["server"]
(3) 收益:镜像从 1.2 GB 降到 12 MB
镜像体积缩小 100 倍,部署下载时间从 5 分钟降到 5 秒,安全漏洞从 200+ 个降到 10 个(编译器不进入生产镜像)。
3. 多阶段构建原理
(1) 单阶段 vs 多阶段对比
graph TB
subgraph Single["Single-stage Build"]
S1["golang:1.22<br/>800 MB base"] --> S2["go build<br/>编译产物"] --> S3["Final Image<br/>1.2 GB<br/>(含编译器+源码)"]
end
subgraph Multi["Multi-stage Build"]
M1["Stage 1: golang:1.22<br/>800 MB (discarded)"] -->|"COPY --from"| M3["Stage 2: alpine:3.19<br/>7 MB base"]
M1 --> M2["go build<br/>编译产物"]
M2 -->|"只复制二进制"| M3
M3 --> M4["Final Image<br/>12 MB<br/>(只有二进制)"]
end
(1) 核心机制
| 阶段 | 用途 | 包含 | 最终镜像 |
|---|---|---|---|
| Builder Stage | 编译/构建 | 编译器、源码、构建工具 | ❌ 丢弃 |
| Runner Stage | 运行 | 只复制必要产物 | ✅ 保留 |
📌 重点: 多阶段构建不是压缩镜像,而是从根源上不把不需要的东西放进镜像。Builder 阶段的层全部丢弃,只有 Runner 阶段的层进入最终镜像。
4. 多阶段构建实战
▶ 示例:Go 多阶段构建(难度⭐⭐)
DOCKERFILE
# ============================================
# Stage 1: Build the Go binary
# ============================================
FROM golang:1.22 AS builder
WORKDIR /src
# Copy dependency files first (cache optimization)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o server
# ============================================
# Stage 2: Create minimal runtime image
# ============================================
FROM alpine:3.19
# Install CA certificates (for HTTPS requests)
RUN apk --no-cache add ca-certificates
# Copy only the compiled binary from builder
COPY --from=builder /src/server /usr/local/bin/server
# Run as non-root user
RUN adduser -D -u 1000 appuser
USER appuser
EXPOSE 8080
CMD ["server"]
BASH
# Build the multi-stage image
docker build -t go-app:1.0 .
# Check the final image size
docker images go-app:1.0
💻 输出:
TEXT
📖 仅展示
REPOSITORY TAG IMAGE ID SIZE
go-app 1.0 a1b2c3d4e5f6 12.5MB
▶ 示例:Node.js 构建→Nginx 运行(难度⭐⭐⭐)
React/Vue 等前端项目:构建阶段生成静态文件,运行阶段只需要 Nginx 提供 HTTP 服务。
DOCKERFILE
# ============================================
# Stage 1: Build React application
# ============================================
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# ============================================
# Stage 2: Serve with Nginx
# ============================================
FROM nginx:1.25-alpine
# Copy built assets from builder stage
COPY --from=builder /app/build /usr/share/nginx/html
# Custom Nginx config for SPA routing
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
💡 提示: 前端项目的多阶段构建效果最显著——node_modules 可能有 500+ MB,但构建产物(HTML/CSS/JS)通常只有 5-20 MB。
▶ 示例:Python venv 复制(难度⭐⭐)
Python 项目也可以用多阶段构建:第一阶段创建虚拟环境,第二阶段只复制虚拟环境和应用代码。
DOCKERFILE
# Stage 1: Build virtual environment
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# Stage 2: Run with venv only
FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY . .
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "app.py"]
5. 运行基镜像选择
(1) 三种极小基镜像对比
| 基镜像 | 大小 | 有 shell | 有包管理器 | 有 libc | 适用场景 |
|---|---|---|---|---|---|
scratch |
0 MB | ❌ | ❌ | ❌ | 静态编译的 Go/Rust 二进制 |
distroless |
2-20 MB | ❌ | ❌ | ✅ (glibc) | Google 出品,极安全 |
alpine |
7 MB | ✅ (ash) | ✅ (apk) | ✅ (musl) | 需要调试时 |
(2) scratch 镜像注意事项
DOCKERFILE
# Scratch: absolutely nothing in the image
FROM scratch
COPY --from=builder /src/server /server
CMD ["/server"]
⚠️ 注意: scratch 镜像没有 shell、没有 ca-certificates、没有时区数据。Go 程序需要
CGO_ENABLED=0 静态编译才能在 scratch 上运行。如果程序需要 HTTPS 请求或日志时间戳,用 alpine 更安全。
6. 多阶段构建高级技巧
▶ 示例:阶段命名和 --target 选择构建(难度⭐⭐)
DOCKERFILE
# Named stages for clarity
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN go build -o server
# Alternative: development stage with hot-reload tools
FROM golang:1.22 AS dev
WORKDIR /src
RUN go install github.com/cosmtrek/air@latest
COPY . .
CMD ["air"]
# Production: minimal runtime
FROM alpine:3.19 AS prod
COPY --from=builder /src/server /usr/local/bin/
CMD ["server"]
BASH
# Build only the dev stage
docker build --target dev -t myapp:dev .
# Build only the prod stage
docker build --target prod -t myapp:prod .
▶ 示例:--target 调试中间产物(难度⭐⭐⭐)
BASH
# Build and inspect the builder stage
docker build --target builder -t myapp:builder .
# Run the builder image to debug build issues
docker run -it myapp:builder bash
# Check the compiled binary
docker run --rm myapp:builder ls -la /src/server
7. 各语言编译产物大小参考
| 语言 | 基础镜像 | 编译产物 | 最终镜像 | 缩减比 |
|---|---|---|---|---|
| Go | golang:1.22 (780 MB) | 静态二进制 12 MB | alpine: 12 MB | 65x |
| Rust | rust:1.75 (1.5 GB) | 静态二进制 8 MB | alpine: 15 MB | 100x |
| Node.js | node:20 (1.1 GB) | build/ 5-20 MB | nginx-alpine: 25 MB | 44x |
| Java | eclipse-temurin:21 (450 MB) | JAR 30 MB | jre-alpine: 170 MB | 2.6x |
| Python | python:3.12 (1 GB) | venv/ 50 MB | slim: 200 MB | 5x |
8. 完整示例:Go Web 应用多阶段构建
DOCKERFILE
# ============================================
# Multi-stage Dockerfile for Go web application
# Stage 1: Build with dependency caching
# Stage 2: Minimal Alpine runtime
# ============================================
# ---------- Stage 1: Builder ----------
FROM golang:1.22-alpine AS builder
# Install git (for go mod download with private repos)
RUN apk add --no-cache git
WORKDIR /src
# Cache: copy dependency files first
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build static binary (CGO_ENABLED=0 for scratch/alpine)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags="-w -s" -o /app/server
# ---------- Stage 2: Runtime ----------
FROM alpine:3.19
# Install runtime dependencies
RUN apk --no-cache add ca-certificates tzdata
# Create non-root user
RUN adduser -D -u 1000 appuser
WORKDIR /app
# Copy only the binary from builder
COPY --from=builder /app/server .
# Set ownership
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:8080/health || exit 1
CMD ["/app/server"]
BASH
# Build
docker build -t go-web:1.0 .
# Run
docker run -d -p 8080:8080 --name go-web go-web:1.0
# Verify size and health
docker images go-web:1.0
docker inspect go-web --format='Size: {{.Size}}, Health: {{.State.Health.Status}}'
💻 输出:
TEXT
📖 仅展示
REPOSITORY TAG SIZE
go-web 1.0 15.2MB
Size: 15974400, Health: healthy
❓ 常见问题
Q 多阶段构建会影响构建速度吗?
A 不会明显影响。Builder 阶段的层仍然被缓存——如果代码没变,
go build 直接用缓存。只有最终镜像不包含 Builder 层。构建速度与单阶段几乎相同,但镜像体积大幅缩小。Q 可以用多阶段构建调试吗?
A 可以。用
--target builder 构建到 Builder 阶段,然后 docker run -it myapp:builder bash 进入调试。也可以在 Dockerfile 中定义一个 dev 阶段,包含调试工具(如 dlv/air)。Q scratch 镜像里没 shell 怎么 debug?
A scratch 没有 shell,无法
docker exec 进入。调试方法:① 用 --target builder 构建调试镜像;② 临时把 FROM scratch 改成 FROM alpine 调试;③ 在主机上用 docker cp 复制日志文件出来。Q 多阶段构建能传输 secret 吗?
A 不要把 secret 写在 Builder 阶段——虽然 Builder 层不在最终镜像中,但
docker history 仍可见。使用 --mount=type=secret(BuildKit 功能)安全传递 secret:RUN --mount=type=secret,id=ssh_key cp /run/secrets/ssh_key /root/.ssh/id_rsa。Q distroless 镜像安全吗?
A 非常安全。Google 维护的 distroless 不包含 shell、包管理器和任何非必要工具,攻击面极小。但调试困难(无 shell),建议开发用 alpine,生产用 distroless(配合日志/监控外部调试)。
Q 为什么 Go 镜像用 alpine 不用 scratch?
A alpine 提供了 ca-certificates(HTTPS 请求必需)和时区数据(日志时间戳必需),还有 shell 用于紧急调试。额外 5 MB 完全值得。只有极致优化场景(嵌入式设备)才用 scratch。
📖 小节
- 多阶段构建:Builder 阶段编译,Runner 阶段只复制产物,镜像从 GB 降到 MB
FROM xxx AS builder命名阶段,COPY --from=builder跨阶段复制- 运行基镜像选择:scratch(0 MB)> distroless(2-20 MB)> alpine(7 MB)
--target可构建指定阶段,用于调试和开发- Go/Rust 编译型语言受益最大(100x 体积缩减),Node 前端项目次之(44x)
CGO_ENABLED=0+-ldflags="-w -s"是 Go 静态编译的标准参数
📝 作业
- 基础题(难度⭐):对比单阶段和多阶段构建的 Go 应用镜像大小——先用
FROM golang:1.22单阶段构建,再用多阶段构建,记录两个镜像的大小差异。 - 进阶题(难度⭐⭐):用多阶段构建一个 React 应用:
node:20-alpine构建 →nginx:1.25-alpine运行。对比 node 基镜像和 nginx 基镜像的最终产物大小。 - 挑战题(难度⭐⭐⭐):用
docker history分析多阶段构建镜像的每一层大小,解释为什么 Builder 阶段的层不在最终镜像中。