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 缓存与缓存失效分析。