DevToolBox免费
博客

Docker 多阶段构建:为生产环境优化镜像

12 分钟作者 DevToolBox

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 / FrameworkSingle-Stage SizeMulti-Stage SizeSavings
Node.js (React app)1.2 GB~150 MB87%
Go (REST API)800 MB~10 MB99%
Python (Django)900 MB~200 MB78%
Java (Spring Boot)1.5 GB~250 MB83%
Rust (binary)2 GB~15 MB99%

优化最佳实践

  1. 按最少到最频繁更改的顺序排列指令。在 COPY . . 之前先 COPY package.json 以最大化缓存命中率。
  2. 使用 alpine 或 slim 基础镜像来减小大小和攻击面。
  3. 在生产阶段始终以非 root 用户运行。
  4. 用 && 组合 RUN 命令以减少层数:RUN apt-get update && apt-get install -y pkg && rm -rf /var/lib/apt/lists/*
  5. 使用 .dockerignore 从构建上下文中排除 node_modules、.git、测试和文档。
  6. 命名你的阶段(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 编译以获得不需要任何其他内容的静态二进制文件。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

🐳Docker Run to Compose{ }JSON Formatter#Hash Generator

相关文章

Kubernetes 入门完全教程 (2026)

从零学习 Kubernetes:Pod、Service、Deployment、ConfigMap 等核心概念。

GitHub Actions 密钥与安全:环境保护、OIDC 认证与最佳实践

GitHub Actions 工作流安全实践:加密密钥管理、环境保护规则、OIDC 云认证与安全加固。