Skip to content

多阶段构建

多阶段构建(Multi-stage Build)是 Docker 17.05+ 引入的功能,允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 都是一个独立的构建阶段。最终镜像只包含最后阶段的内容,大幅减小镜像体积。

为什么需要多阶段构建

传统方式的问题

开发依赖        编译工具        源代码        测试文件
   +              +              +             +
                    最终镜像(体积庞大)

传统做法的痛点:

  • 编译工具(gcc、maven、node 等)包含在最终镜像中,体积大
  • 源代码、测试文件等不必要内容暴露在镜像中
  • 安全面更大

多阶段构建方案

阶段一(构建):包含所有构建工具 → 编译出产物
阶段二(运行):只包含运行时 + 编译产物 → 最终镜像

Go 应用示例

Go 编译后是静态二进制,最终镜像可以从 scratch 开始:

dockerfile
# ===== 阶段一:编译 =====
FROM golang:1.21-alpine AS builder

WORKDIR /build

# 先复制依赖文件(利用缓存)
COPY go.mod go.sum ./
RUN go mod download

# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app .

# ===== 阶段二:运行 =====
FROM scratch AS runtime

# 从构建阶段复制二进制
COPY --from=builder /build/app /app

# 如果需要 HTTPS,还需要 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

EXPOSE 8080
CMD ["/app"]

对比效果:

单阶段(golang:alpine)多阶段(scratch)
镜像大小~600MB~8MB
安全面极小

Node.js 应用示例

dockerfile
# ===== 阶段一:安装依赖 =====
FROM node:20-alpine AS deps

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# ===== 阶段二:构建 =====
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

# ===== 阶段三:运行 =====
FROM node:20-alpine AS runtime

ENV NODE_ENV=production

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 从 deps 阶段复制生产依赖
COPY --from=deps --chown=appuser:appgroup /app/node_modules ./node_modules
# 从 builder 阶段复制构建产物
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --chown=appuser:appgroup package.json .

USER appuser

EXPOSE 3000
CMD ["node", "dist/main.js"]

Java Maven 应用示例

dockerfile
# ===== 阶段一:构建 =====
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# 先复制 pom.xml 下载依赖(利用缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源码并构建
COPY src ./src
RUN mvn package -DskipTests

# ===== 阶段二:运行 =====
FROM eclipse-temurin:17-jre-alpine AS runtime

RUN addgroup -S spring && adduser -S spring -G spring

WORKDIR /app

COPY --from=builder --chown=spring:spring /build/target/*.jar app.jar

USER spring

EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

Python 应用示例

dockerfile
# ===== 阶段一:构建依赖 =====
FROM python:3.12-slim AS builder

WORKDIR /build

# 安装编译依赖
RUN apt-get update && apt-get install -y \
    gcc \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# ===== 阶段二:运行 =====
FROM python:3.12-slim AS runtime

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

RUN addgroup --system appgroup && adduser --system --group appuser

WORKDIR /app

# 从构建阶段复制已安装的 Python 包
COPY --from=builder --chown=appuser:appgroup /root/.local /home/appuser/.local

COPY --chown=appuser:appgroup . .

USER appuser

ENV PATH=/home/appuser/.local/bin:$PATH

EXPOSE 5000
CMD ["python", "app.py"]

阶段引用方式

按名称引用

dockerfile
FROM node:20 AS builder
# ...

FROM nginx:alpine AS runtime
COPY --from=builder /app/dist /usr/share/nginx/html

按索引引用

dockerfile
FROM node:20    # 阶段 0
# ...

FROM nginx:alpine
COPY --from=0 /app/dist /usr/share/nginx/html  # 引用第 0 阶段

引用外部镜像

dockerfile
# 直接从任意镜像复制文件
COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf
COPY --from=golang:1.21 /usr/local/go /usr/local/go

构建指定阶段

bash
# 只构建到指定阶段(用于调试)
docker build --target builder -t myapp-builder .

# 正常构建最终阶段
docker build -t myapp:latest .

利用缓存加速构建

多阶段构建时合理安排指令顺序可以最大化缓存利用:

dockerfile
FROM node:20-alpine AS deps

WORKDIR /app

# 先复制依赖文件 ← 变化频率低,缓存稳定
COPY package*.json ./
RUN npm ci

# 再复制源码 ← 变化频率高,修改代码不影响依赖缓存
COPY . .
RUN npm run build

实际效果对比

bash
# 构建并查看镜像大小
docker build -t myapp-single -f Dockerfile.single .
docker build -t myapp-multi -f Dockerfile.multi .

docker images | grep myapp
# myapp-single    latest    xxx   600MB  (含 node:20 完整环境)
# myapp-multi     latest    xxx   80MB   (仅含运行时)

总结

多阶段构建的核心价值:

  1. 减小镜像体积:最终镜像只包含运行时所需内容
  2. 提升安全性:构建工具、源码、测试代码不进入最终镜像
  3. 清晰的构建流程:构建和运行关注点分离

常见组合:

  • Gogolangscratchalpine
  • Node.jsnode 构建 → node:alpine 运行
  • Javamavenjre-alpine
  • Pythonpython:slim 安装依赖 → python:slim 运行

下一节介绍镜像构建的最佳实践。