前言
在日常开发中,你是否遇到过这样的问题:
- Docker 镜像动辄几个 GB,拉取部署要等好几分钟
- 每次构建都要重新安装所有依赖,CI/CD 流水线慢得让人抓狂
- 构建环境和运行环境混在一起,镜像里塞了大量不需要的工具
这些问题的根源在于:构建产物和运行环境没有分离。Docker 的多阶段构建(Multi-stage Build)正是为解决这一痛点而生的利器。本文将通过三个真实语言栈的案例,带你掌握这一核心技术。
什么是多阶段构建?
多阶段构建的核心思想非常简单:一个 Dockerfile 中使用多个 FROM 指令,每个阶段只保留最终需要的文件。
1
2
3
4
5
6
7
8
|
阶段1(构建阶段) 阶段2(运行阶段)
┌─────────────────┐ ┌─────────────────┐
│ FROM ubuntu │ │ FROM alpine │
│ 安装JDK/Maven │ ──→ │ 只复制JAR包 │
│ 编译代码 │ │ 运行应用 │
│ 生成产物 │ │ │
│ (~800MB) │ │ (~200MB) │
└─────────────────┘ └─────────────────┘
|
箭头表示只复制构建产物(如二进制文件、编译结果),而非整个构建环境。
案例一:Java Spring Boot 应用
Spring Boot 应用是最典型的受益场景。传统方式用 openjdk:17 基础镜像,最终镜像通常在 500MB 以上。
传统方式(反面教材)
1
2
3
4
5
6
7
8
9
|
# ❌ 不推荐:单一阶段
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests
# 最终镜像包含完整的Maven、JDK、源码、缓存等
EXPOSE 8080
CMD ["java", "-jar", "target/app.jar"]
|
这个镜像包含了完整的 Maven 构建工具链,体积轻松超过 1GB,而运行时根本不需要 Maven。
多阶段构建方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
# ✅ 推荐:多阶段构建
# ===== 阶段1:构建 =====
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build
# 利用Docker层缓存,先复制pom.xml单独下载依赖
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 再复制源码进行编译
COPY src ./src
RUN mvn clean package -DskipTests
# 利用Spring Boot的分层Jar功能进一步优化
RUN java -Djarmode=layertools -jar target/*.jar extract --destination extracted
# ===== 阶段2:运行 =====
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# 从构建阶段复制分层后的应用(便于增量更新)
COPY --from=builder /build/extracted/dependencies/ ./
COPY --from=builder /build/extracted/spring-boot-loader/ ./
COPY --from=builder /build/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/extracted/application/ ./
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
|
关键优化点:
- 依赖分离缓存:先复制
pom.xml 下载依赖,再复制源码。只有源码变化时才重新编译,依赖层被缓存。
- JRE 替代 JDK:运行时只用 JRE-Alpine,比 JDK 省了约 300MB。
- Spring Boot 分层 JAR:将应用按依赖层次拆分,Docker 可以只更新变化的层。
| 指标 |
传统方式 |
多阶段构建 |
| 镜像大小 |
~800MB |
~200MB |
| 构建时间(首次) |
3分钟 |
3分钟 |
| 构建时间(改代码) |
3分钟 |
30秒(利用层缓存) |
案例二:Python FastAPI 应用
Python 应用的镜像优化同样立竿见影。
多阶段构建方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
# ===== 阶段1:构建wheel包 =====
FROM python:3.12-slim AS builder
WORKDIR /build
# 安装构建依赖
RUN pip install --no-cache-dir poetry==1.8.0
# 先复制依赖声明文件,利用层缓存
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
# 用pip下载所有依赖到独立目录
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# ===== 阶段2:运行 =====
FROM python:3.12-slim
WORKDIR /app
# 安装系统级运行时依赖(仅需要的)
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
&& rm -rf /var/lib/apt/lists/*
# 从builder阶段复制预安装的Python包
COPY --from=builder /install /usr/local
# 复制应用代码
COPY app/ ./app/
# 创建非root用户运行
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
Python 场景的关键技巧:
- Poetry 导出 requirements.txt:Poetry 是优秀的依赖管理工具,但生产环境直接用 pip 安装更轻量。
--prefix=/install:将包安装到独立目录,方便跨阶段复制,避免带入 pip 等构建工具。
- Alpine vs Slim:Alpine 镜像虽小但 musl libc 可能导致兼容性问题,推荐用
slim 变体。
案例三:Node.js Next.js 应用
前端 SSR 应用的优化更加复杂,但效果也最明显。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
# ===== 阶段1:安装依赖 =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# ===== 阶段2:构建应用 =====
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js的standalone模式只输出必要文件
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx next build
# ===== 阶段3:运行 =====
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 只复制standalone输出
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD ["node", "server.js"]
|
需要在 next.config.js 中启用 standalone 模式:
1
2
3
4
5
|
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
|
高级优化技巧
1. 使用 COPY --link 加速并行构建
1
2
3
4
|
# --link 允许COPY指令与之前的层并行执行
COPY --link requirements.txt .
RUN pip install -r requirements.txt
COPY --link . .
|
Docker 24.0+ 支持此特性,可将构建速度提升 20%-50%。
2. 使用 BUILDKIT 语法加速
1
2
3
4
5
6
7
8
|
# syntax=docker/dockerfile:1
# 启用Mount缓存,让apt/pip/npm等包管理器的缓存持久化
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y curl
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
|
3. .dockerignore 配合使用
1
2
3
4
5
6
7
|
.git
node_modules
*.md
.env*
target/
__pycache__
.pytest_cache
|
确保构建上下文尽可能小,加速 COPY 操作。
4. 多阶段构建中的安全扫描
1
2
3
4
|
# 可以专门加一个安全扫描阶段
FROM builder AS security-scan
RUN --mount=type=cache,target=/root/.cache/trivy \
trivy filesystem --exit-code 1 --severity HIGH,CRITICAL .
|
总结
| 优化手段 |
预期效果 |
| 多阶段构建分离构建/运行环境 |
镜像减少 60%-80% |
| 层缓存 + 依赖分离 |
重复构建时间减少 70% |
| Alpine/Slim 基础镜像 |
进一步减少 30%-50% |
| 非 root 用户运行 |
安全加固 |
.dockerignore |
构建上下文减少 50%+ |
多阶段构建不是什么高深技巧,而是每个 Docker 用户都应该掌握的基本功。它带来的镜像瘦身和构建加速效果是实实在在的——从 GB 到 MB,从分钟到秒级。下次写 Dockerfile 时,不妨试试这些优化方法。
📌 本文代码已适配 Docker BuildKit(默认启用),推荐 Docker 24.0+ 版本获得最佳体验。