Docker 多阶段构建允许你在单个 Dockerfile 中使用多个 FROM 语句。每个阶段都是全新的环境——你在独立阶段中编译、测试和打包应用程序,然后只将最终工件复制到最小化的生产镜像中。结果:镜像大小显著减小、安全性更好、构建过程更清晰。
为什么使用多阶段构建?
传统 Dockerfile 会将构建过程中使用的所有内容都打包进去——编译器、开发依赖、测试框架。多阶段构建将构建环境与运行时环境隔离。
- 镜像大小减小:Node.js 应用可以从 1.2 GB 缩减到 150 MB
- 安全性:更少的包 = 更小的攻击面。生产环境中无编译器、无构建工具
- 构建缓存:每个阶段独立缓存,加速后续构建
- 关注点分离:构建、测试和运行时清晰分离
Node.js:经典模式
最常见的用例:编译 TypeScript 或用 webpack 打包,然后只复制 dist 文件夹和生产依赖。
# Single-stage build (bad — ships with devDependencies and build tools)
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install # includes devDependencies
COPY . .
RUN npm run build
# Final image: ~1.2 GB (includes all node_modules, source, build tools)
---
# Multi-stage build (good — minimal production image)
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # clean install
COPY . .
RUN npm run build
# Stage 2: Production
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# Only copy what's needed
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production # only production deps
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node # run as non-root
CMD ["node", "dist/server.js"]
# Final image: ~150 MB (85% reduction!)Go:静态二进制文件(FROM scratch)
Go 生成可在 FROM scratch 容器中运行的静态二进制文件——只有二进制文件,没有操作系统,没有 shell。这是最小的可能镜像。
# Go: the ultimate multi-stage build
# Go compiles to a static binary — final image can be FROM scratch
# Stage 1: Build
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Download dependencies first (better cache)
COPY go.mod go.sum ./
RUN go mod download
# Build the binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/server
# Stage 2: Ultra-minimal runtime (zero OS!)
FROM scratch AS production
# Copy SSL certificates for HTTPS calls
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
# Copy just the binary
COPY --from=builder /app/main /main
EXPOSE 8080
ENTRYPOINT ["/main"]
# Final image: ~10 MB (just the binary + CA certs)Python:编译 Wheels 模式
Python 通常需要系统库(gcc、libpq)来编译 C 扩展。在第一阶段构建 wheels,在第二阶段安装预构建的 wheels,无需构建工具。
# Python: multi-stage with dependency caching
# Stage 1: Build wheels (compiled dependencies)
FROM python:3.12-slim AS builder
WORKDIR /app
# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev && rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
# Build wheels so Stage 2 doesn't need gcc
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt
# Stage 2: Runtime
FROM python:3.12-slim AS production
WORKDIR /app
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# Only install runtime deps (no gcc needed)
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 && rm -rf /var/lib/apt/lists/*
# Install pre-built wheels
COPY --from=builder /app/wheels /wheels
RUN pip install --no-cache /wheels/*
COPY . .
# Create non-root user
RUN addgroup --system app && adduser --system --group app
USER app
CMD ["gunicorn", "myapp.wsgi:application", "--bind", "0.0.0.0:8000"]包含测试阶段
添加一个专门的测试阶段来运行你的测试套件。在 CI 中使用 --target 只构建特定阶段。
# Multi-stage with test stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
# Build stage
FROM deps AS builder
COPY . .
RUN npm run build
# Test stage (run in CI, skip in production builds)
FROM builder AS test
RUN npm run test
RUN npm run lint
# Production stage (the default target)
FROM node:20-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules # share deps
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
# Build commands:
# docker build . # builds production (default)
# docker build --target test . # runs tests only
# docker build --target builder . # build artifacts only跨阶段的构建参数
ARG 值必须在使用它们的每个阶段重新声明。ENV 变量也不会跨阶段传递。
# Using ARG and ENV across stages
# Build arguments (available at build time)
ARG NODE_VERSION=20
ARG APP_VERSION=1.0.0
FROM node:${NODE_VERSION}-alpine AS builder
WORKDIR /app
ARG APP_VERSION
# Bake version into the binary at build time
ENV APP_VERSION=${APP_VERSION}
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:${NODE_VERSION}-alpine AS production
WORKDIR /app
# ARG must be redeclared in each stage where you use it
ARG APP_VERSION
LABEL version="${APP_VERSION}"
ENV NODE_ENV=production
ENV APP_VERSION=${APP_VERSION}
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
# Build with:
# docker build --build-arg NODE_VERSION=22 --build-arg APP_VERSION=2.1.0 .按语言的大小对比
| Language / Framework | Single-Stage Size | Multi-Stage Size | Savings |
|---|---|---|---|
| Node.js (React app) | 1.2 GB | ~150 MB | 87% |
| Go (REST API) | 800 MB | ~10 MB | 99% |
| Python (Django) | 900 MB | ~200 MB | 78% |
| Java (Spring Boot) | 1.5 GB | ~250 MB | 83% |
| Rust (binary) | 2 GB | ~15 MB | 99% |
优化最佳实践
- 按最少到最频繁更改的顺序排列指令。在 COPY . . 之前先 COPY package.json 以最大化缓存命中率。
- 使用 alpine 或 slim 基础镜像来减小大小和攻击面。
- 在生产阶段始终以非 root 用户运行。
- 用 && 组合 RUN 命令以减少层数:RUN apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*
- 使用 .dockerignore 从构建上下文中排除 node_modules、.git、测试和文档。
- 命名你的阶段(AS builder),以便可以显式引用它们,并在 CI 中使用 --target。
常见问题
我可以在 CI 中缓存多阶段构建吗?
可以。使用 Docker BuildKit 与 cache-from:docker buildx build --cache-from type=gha --cache-to type=gha,mode=max。GitHub Actions 通过 actions/cache 和 docker/build-push-action 具有原生 BuildKit 缓存支持。每个阶段独立缓存,因此只有已更改的阶段才会重建。
如何在阶段之间共享数据?
使用 COPY --from=stage_name 从命名阶段复制文件。你只能复制文件,不能复制环境变量或构建缓存。对于在不相关镜像之间共享,使用带有 BuildKit 的构建卷(docker build --mount)。
--target 会跳过未使用的阶段吗?
是的。当你指定 --target builder 时,Docker 只构建到目标阶段(包含目标阶段)。后续阶段完全被跳过。当你只需要运行测试而无需构建最终生产镜像时,这可以加快 CI 速度。
我可以有多个最终阶段(不同的部署目标)吗?
可以。你可以有多个共享公共构建器阶段的"最终"阶段。使用 --target 构建你需要的那个。常见模式:一个带有 Node.js 的生产阶段和一个运行数据库迁移的迁移阶段,两者都从相同的编译输出构建。
如何在 FROM scratch 阶段使用私有注册表镜像?
FROM scratch 实际上创建了一个空镜像——没有操作系统、没有 shell、没有包管理器。你必须复制二进制文件所需的一切:二进制文件本身、SSL 证书(用于 HTTPS)、时区数据(如果需要)和任何动态库。对于 Go,使用 CGO_ENABLED=0 编译以获得不需要任何其他内容的静态二进制文件。