Contents

Docker多阶段构建实战:优化镜像大小与构建速度的完整指南

前言

在日常开发中,你是否遇到过这样的问题:

  • 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"]

关键优化点:

  1. 依赖分离缓存:先复制 pom.xml 下载依赖,再复制源码。只有源码变化时才重新编译,依赖层被缓存。
  2. JRE 替代 JDK:运行时只用 JRE-Alpine,比 JDK 省了约 300MB。
  3. 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 场景的关键技巧:

  1. Poetry 导出 requirements.txt:Poetry 是优秀的依赖管理工具,但生产环境直接用 pip 安装更轻量。
  2. --prefix=/install:将包安装到独立目录,方便跨阶段复制,避免带入 pip 等构建工具。
  3. 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
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+ 版本获得最佳体验。