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 多阶段对比

100%
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。

📖 小节


📝 作业

  1. 基础题(难度⭐):对比单阶段和多阶段构建的 Go 应用镜像大小——先用 FROM golang:1.22 单阶段构建,再用多阶段构建,记录两个镜像的大小差异。
  2. 进阶题(难度⭐⭐):用多阶段构建一个 React 应用:node:20-alpine 构建 → nginx:1.25-alpine 运行。对比 node 基镜像和 nginx 基镜像的最终产物大小。
  3. 挑战题(难度⭐⭐⭐):用 docker history 分析多阶段构建镜像的每一层大小,解释为什么 Builder 阶段的层不在最终镜像中。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏