强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Dockerfile 写作精讲 / 09 - 多阶段构建

09 - 多阶段构建:分阶段编排与最小化镜像

9.1 什么是多阶段构建

多阶段构建(Multi-Stage Build)允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 开始一个新的构建阶段。最终镜像只包含最后一个阶段(或指定阶段)的内容。

核心价值

单阶段构建:
┌────────────────────────────────────────┐
│ 基础镜像 + 构建工具 + 源码 + 构建产物  │ = 数百 MB
└────────────────────────────────────────┘

多阶段构建:
阶段 1 (builder):  基础镜像 + 构建工具 + 源码 → 构建产物
阶段 2 (production): 精简镜像 + 构建产物        = 几十 MB
对比 单阶段 多阶段
镜像体积 大(含构建工具) 小(仅含运行时)
攻击面 大(含编译器、源码)
构建缓存 ⚠️ 与运行时混合 ✅ 独立管理
Dockerfile 复杂度

9.2 基本语法

# 阶段一:构建
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /server .

# 阶段二:生产(默认只保留此阶段)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /server /server
ENTRYPOINT ["/server"]

阶段命名

# 使用 AS 命名(推荐)
FROM node:20-alpine AS builder
FROM node:20-alpine AS tester
FROM nginx:alpine AS production

# 不命名时使用索引(0, 1, 2...)
FROM node:20-alpine       # 阶段 0
FROM nginx:alpine         # 阶段 1

# 通过索引引用
COPY --from=0 /app/dist /usr/share/nginx/html

9.3 从不同来源复制

# 从当前构建的其他阶段复制
COPY --from=builder /app/dist /usr/share/nginx/html

# 从外部镜像复制(无需构建)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf

# 从外部镜像复制二进制工具
COPY --from=hashicorp/terraform:1.7 /bin/terraform /usr/local/bin/terraform

# 使用阶段索引
COPY --from=0 /app/dist /usr/share/nginx/html

实用:从外部镜像获取工具

FROM alpine:3.19

# 从不同镜像复制工具
COPY --from=docker:24-cli /usr/local/bin/docker /usr/local/bin/docker
COPY --from=docker/compose-bin:latest /docker-compose /usr/local/bin/docker-compose
COPY --from=golang:1.22-alpine /usr/local/go/bin/go /usr/local/bin/go
COPY --from=alpine/git:latest /usr/bin/git /usr/local/bin/git

RUN apk add --no-cache bash curl
CMD ["/bin/bash"]

9.4 Go 应用多阶段构建

# ===== 阶段一:构建 =====
FROM golang:1.22-alpine AS builder

WORKDIR /src

# 依赖缓存层
COPY go.mod go.sum ./
RUN go mod download

# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build \
    -ldflags="-s -w -X main.version=$(git describe --tags --always)" \
    -o /server \
    ./cmd/server

# ===== 阶段二:测试(可选) =====
FROM builder AS tester
RUN go test ./... -v -coverprofile=coverage.out

# ===== 阶段三:生产 =====
FROM gcr.io/distroless/static-debian12:nonroot

COPY --from=builder /server /server
COPY --from=builder /src/configs /configs

EXPOSE 8080
ENTRYPOINT ["/server"]

仅构建测试阶段

# 构建并运行测试
docker build --target tester -t myapp:test .

# 仅构建生产镜像(跳过测试阶段)
docker build --target production -t myapp:prod .

9.5 Node.js 应用多阶段构建

# ===== 阶段一:依赖安装 =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

# ===== 阶段二:构建 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

# ===== 阶段三:生产依赖 =====
FROM node:20-alpine AS production-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production

# ===== 阶段四:生产 =====
FROM node:20-alpine AS production
WORKDIR /app

RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

# 仅复制生产依赖和构建产物
COPY --from=production-deps --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json ./

USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]

对比:单阶段 vs 多阶段

指标 单阶段 多阶段(以上)
最终镜像体积 ~800MB ~150MB
包含 devDependencies
包含源码
包含构建工具

9.6 Java 应用多阶段构建

# ===== 阶段一:构建 =====
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app

# 复制 Maven 包装器和 POM
COPY .mvn .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline

# 复制源码并打包
COPY src ./src
RUN ./mvnw package -DskipTests --no-transfer-progress

# 使用 jlink 创建自定义 JRE(可选,进一步减小体积)
RUN $JAVA_HOME/bin/jlink \
    --add-modules java.base,java.net.http,java.logging \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=2 \
    --output /jre

# ===== 阶段二:生产 =====
FROM alpine:3.19

# 使用自定义 JRE
COPY --from=builder /jre /opt/jre
ENV PATH="/opt/jre/bin:${PATH}"

# 创建非 root 用户
RUN addgroup -g 1001 appgroup && \
    adduser -u 1001 -G appgroup -s /bin/sh -D appuser

WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /app/target/*.jar app.jar

USER appuser
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]

9.7 Python 应用多阶段构建

# ===== 阶段一:构建 =====
FROM python:3.12-slim AS builder

WORKDIR /app

# 安装构建依赖
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc libpq-dev && \
    rm -rf /var/lib/apt/lists/*

# 安装 Python 依赖
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ===== 阶段二:生产 =====
FROM python:3.12-slim AS production

# 安装运行时依赖(不含编译器)
RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 curl && \
    rm -rf /var/lib/apt/lists/*

# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# 从 builder 复制已安装的 Python 包
COPY --from=builder /install /usr/local

# 复制应用代码
COPY --chown=appuser:appuser . .

USER appuser
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s \
    CMD curl -f http://localhost:8000/health || exit 1
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "4"]

9.8 条件阶段与构建矩阵

条件执行

FROM alpine:3.19 AS base
RUN apk add --no-cache bash

FROM base AS debug
RUN apk add --no-cache vim strace gdb

FROM base AS production
# 不安装调试工具
COPY app /app
# 构建 debug 版本
docker build --target debug -t myapp:debug .

# 构建 production 版本
docker build --target production -t myapp:prod .

ARG 控制阶段行为

ARG BUILD_ENV=production

FROM node:20-alpine AS builder
ARG BUILD_ENV
WORKDIR /app
COPY . .
RUN if [ "$BUILD_ENV" = "development" ]; then \
        npm install; \
    else \
        npm ci --production; \
    fi && \
    npm run build

FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

9.9 构建目标选择

# 查看所有阶段
docker build --call print-targets .

# 构建特定阶段
docker build --target builder -t myapp:build .
docker build --target production -t myapp:prod .

阶段命名建议

名称 用途
base 基础配置
deps 依赖安装
builder 编译构建
tester 测试运行
production 生产镜像
debug 调试镜像

9.10 常见错误与排查

错误 原因 解决方案
文件未找到 –from 引用了错误的阶段 检查阶段名称或索引
镜像体积未减小 未使用多阶段或复制了不必要的文件 确保只复制运行时需要的文件
构建缓慢 阶段划分不合理 将变化频率低的操作放前面
权限问题 跨阶段复制时用户不匹配 使用 –chown 或数字 UID

9.11 扩展阅读


上一章08 - USER 与权限 下一章10 - 缓存策略 — 缓存挂载、BuildKit 缓存与缓存失效分析。