Skip to content

多阶段构建详解

多阶段构建是 Docker 17.05 引入的强大特性,允许在单个 Dockerfile 中使用多个 FROM 语句。这种构建方式可以显著减小最终镜像体积,同时保持构建环境的完整性。

目录

  1. 多阶段构建基础
  2. 构建优化策略
  3. 减小镜像体积
  4. 开发 vs 生产构建
  5. 高级多阶段技巧
  6. 实际应用案例

多阶段构建基础

1.1 什么是多阶段构建

传统构建的问题:

dockerfile
# ❌ 单阶段构建 - 包含所有构建工具和依赖
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]
# 镜像大小: ~1GB

多阶段构建解决方案:

dockerfile
# ✅ 多阶段构建 - 只保留最终产物
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp

FROM alpine:3.18
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/myapp .
CMD ["./myapp"]
# 镜像大小: ~20MB

1.2 基本语法

dockerfile
# 阶段 1: 构建环境
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# 阶段 2: 生产环境
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

1.3 阶段命名和引用

dockerfile
# 使用 AS 命名阶段
FROM python:3.11 AS dependencies
# ...

FROM python:3.11-slim AS test
COPY --from=dependencies /root/.local /root/.local
# ...

FROM python:3.11-slim AS production
COPY --from=dependencies /root/.local /root/.local
# ...

1.4 阶段间复制

dockerfile
# 从指定阶段复制
COPY --from=builder /app/dist /usr/share/nginx/html

# 从之前阶段复制(使用索引)
COPY --from=0 /app/dist /usr/share/nginx/html

# 复制多个文件
COPY --from=builder \
  /app/dist \
  /app/package.json \
  /app/README.md \
  /usr/share/nginx/html/

# 使用通配符
COPY --from=builder /app/dist/* /usr/share/nginx/html/

# 复制并更改所有者
COPY --from=builder --chown=nginx:nginx /app/dist /usr/share/nginx/html

构建优化策略

2.1 依赖缓存优化

dockerfile
# 策略: 先复制依赖文件,利用缓存层
FROM node:18-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18-alpine AS production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/main.js"]

2.2 并行构建

dockerfile
# 并行构建前端和后端
FROM node:18-alpine AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

FROM golang:1.21-alpine AS backend-builder
WORKDIR /backend
COPY backend/go.mod backend/go.sum ./
RUN go mod download
COPY backend/ .
RUN go build -o server

FROM alpine:3.18
WORKDIR /app
COPY --from=frontend-builder /frontend/dist ./static
COPY --from=backend-builder /backend/server .
EXPOSE 8080
CMD ["./server"]

2.3 条件构建

dockerfile
# syntax=docker/dockerfile:1
FROM node:18-alpine AS base

FROM base AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM dependencies AS development
ENV NODE_ENV=development
COPY . .
CMD ["npm", "run", "dev"]

FROM dependencies AS build
ENV NODE_ENV=production
COPY . .
RUN npm run build

FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html

构建特定目标:

bash
# 构建开发版本
docker build --target development -t myapp:dev .

# 构建生产版本
docker build --target production -t myapp:prod .

2.4 构建参数优化

dockerfile
# syntax=docker/dockerfile:1
ARG NODE_VERSION=18

FROM node:${NODE_VERSION}-alpine AS builder
ARG BUILD_ENV=production
ENV NODE_ENV=${BUILD_ENV}
WORKDIR /app
COPY package*.json ./
RUN if [ "$BUILD_ENV" = "development" ]; then \
        npm ci; \
    else \
        npm ci --only=production; \
    fi
COPY . .
RUN npm run build

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

构建命令:

bash
# 开发构建
docker build --build-arg BUILD_ENV=development -t myapp:dev .

# 生产构建
docker build --build-arg BUILD_ENV=production -t myapp:prod .

减小镜像体积

3.1 基础镜像选择

dockerfile
# 策略: 构建用完整镜像,运行用最小镜像

# Go 应用
FROM golang:1.21-alpine AS builder
RUN apk add --no-cache git
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o app

FROM scratch
COPY --from=builder /app/app /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
ENTRYPOINT ["/app"]
# 镜像大小: ~5MB

3.2 静态编译

dockerfile
# Go 静态编译
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -a -installsuffix cgo \
    -ldflags="-w -s -extldflags '-static'" \
    -o myapp

FROM scratch
COPY --from=builder /app/myapp /myapp
COPY --from=builder /etc/passwd /etc/passwd
USER 1000
ENTRYPOINT ["/myapp"]

3.3 清理构建产物

dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt && \
    find /root/.local -type f -name "*.pyc" -delete && \
    find /root/.local -type d -name "__pycache__" -delete && \
    find /root/.local -type f -name "*.txt" -delete && \
    find /root/.local -type f -name "*.md" -delete

FROM python:3.11-alpine
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

3.4 使用 distroless 镜像

dockerfile
# Python 应用
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt
COPY . .

FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=builder /root/.local /home/nonroot/.local
COPY --from=builder /app /app
ENV PYTHONPATH=/home/nonroot/.local/lib/python3.11/site-packages
USER nonroot
CMD ["app.py"]

开发 vs 生产构建

4.1 开发环境构建

dockerfile
# syntax=docker/dockerfile:1
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./

FROM base AS dependencies
RUN npm ci

FROM dependencies AS development
ENV NODE_ENV=development
COPY . .
# 开发工具
RUN npm install -g nodemon
# 源代码挂载点
VOLUME ["/app/src"]
EXPOSE 3000
CMD ["npm", "run", "dev"]

4.2 生产环境构建

dockerfile
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./

FROM base AS dependencies
RUN npm ci --only=production

FROM dependencies AS build
COPY . .
RUN npm run build

FROM node:18-alpine AS production
ENV NODE_ENV=production
WORKDIR /app
# 只复制生产依赖
COPY --from=dependencies /app/node_modules ./node_modules
# 复制构建产物
COPY --from=build /app/dist ./dist
COPY package.json ./
# 安全设置
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
CMD ["node", "dist/main.js"]

4.3 测试阶段

dockerfile
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt requirements-test.txt ./
RUN pip install --user -r requirements.txt

FROM python:3.11-slim AS test
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
RUN pip install --user -r requirements-test.txt && \
    pytest --cov=app tests/

FROM python:3.11-slim AS production
WORKDIR /app
COPY --from=builder /root/.local /root/.local
COPY . .
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

4.4 完整的 CI/CD 流程

dockerfile
# syntax=docker/dockerfile:1
ARG NODE_VERSION=18

# 依赖安装
FROM node:${NODE_VERSION}-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

# 代码检查
FROM deps AS lint
COPY . .
RUN npm run lint

# 单元测试
FROM deps AS unit-test
COPY . .
RUN npm run test:unit

# 集成测试
FROM deps AS integration-test
COPY . .
RUN npm run test:integration

# 构建
FROM deps AS build
COPY . .
RUN npm run build

# 安全扫描
FROM deps AS security-scan
COPY . .
RUN npm audit --audit-level=moderate

# 生产镜像
FROM node:${NODE_VERSION}-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
# 只复制生产依赖
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
# 复制构建产物
COPY --from=build /app/dist ./dist
# 安全设置
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
USER nodejs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
CMD ["node", "dist/main.js"]

高级多阶段技巧

5.1 使用 BuildKit 缓存

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY . .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -e .

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /app /app
CMD ["python", "app.py"]

5.2 使用秘密挂载

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
# 使用秘密挂载访问私有仓库
RUN --mount=type=secret,id=pip_config \
    pip install --config-file=/run/secrets/pip_config -r requirements.txt
COPY . .
RUN pip install .

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
COPY --from=builder /app /app
CMD ["python", "app.py"]

构建命令:

bash
docker build --secret id=pip_config,src=$HOME/.pip/pip.conf -t myapp .

5.3 使用 SSH 挂载

dockerfile
# syntax=docker/dockerfile:1
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
# 使用 SSH 挂载访问私有仓库
RUN --mount=type=ssh,id=github \
    go mod download
COPY . .
RUN go build -o app

FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]

构建命令:

bash
eval $(ssh-agent)
ssh-add ~/.ssh/id_rsa
docker build --ssh default -t myapp .

5.4 多平台构建

dockerfile
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app

FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/app .
CMD ["./app"]

构建命令:

bash
docker buildx build \
  --platform linux/amd64,linux/arm64,linux/arm/v7 \
  -t myapp:latest \
  --push .

实际应用案例

6.1 Node.js 全栈应用

dockerfile
# syntax=docker/dockerfile:1
# 前端构建
FROM node:18-alpine AS frontend-builder
WORKDIR /frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build

# 后端构建
FROM node:18-alpine AS backend-builder
WORKDIR /backend
COPY backend/package*.json ./
RUN npm ci --only=production
COPY backend/ .

# 生产镜像
FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production

# 复制后端
COPY --from=backend-builder /backend/node_modules ./node_modules
COPY --from=backend-builder /backend/package.json ./
COPY --from=backend-builder /backend/src ./src

# 复制前端静态文件
COPY --from=frontend-builder /frontend/dist ./public

# 安全设置
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000
CMD ["node", "src/server.js"]

6.2 Python 数据科学应用

dockerfile
# syntax=docker/dockerfile:1
FROM python:3.11-slim AS builder

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

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

# 数据预处理
FROM builder AS data-processor
COPY data/ ./data/
COPY scripts/preprocess.py ./
RUN python preprocess.py

# 模型训练
FROM builder AS model-trainer
COPY --from=data-processor /app/processed_data ./processed_data
COPY scripts/train.py ./
RUN python train.py

# 生产镜像
FROM python:3.11-slim AS production
WORKDIR /app

# 复制依赖
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

# 复制模型和数据
COPY --from=model-trainer /app/models ./models
COPY --from=data-processor /app/processed_data ./data

# 复制应用代码
COPY src/ ./src/

EXPOSE 8000
CMD ["python", "-m", "src.api"]

6.3 Java Spring Boot 应用

dockerfile
# syntax=docker/dockerfile:1
FROM eclipse-temurin:17-jdk-alpine AS builder
WORKDIR /app

# 复制 Maven 包装器
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .

# 下载依赖(利用缓存)
RUN ./mvnw dependency:go-offline

# 复制源码并构建
COPY src src
RUN ./mvnw package -DskipTests && \
    mkdir -p target/dependency && \
    (cd target/dependency; jar -xf ../*.jar)

# 生产镜像
FROM eclipse-temurin:17-jre-alpine
VOLUME /tmp
ARG DEPENDENCY=/app/target/dependency

# 复制依赖
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=builder ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=builder ${DEPENDENCY}/BOOT-INF/classes /app

# 安全设置
RUN addgroup -S spring && adduser -S spring -G spring
USER spring:spring

EXPOSE 8080
ENTRYPOINT ["java", "-cp", "app:app/lib/*", "com.example.Application"]

6.4 对比数据

应用类型单阶段镜像多阶段镜像减小比例
Go 应用1.2 GB15 MB98.8%
Node.js 应用1.1 GB180 MB83.6%
Python 应用1.3 GB150 MB88.5%
Java 应用800 MB220 MB72.5%
Rust 应用2.0 GB25 MB98.8%

下一步

基于 MIT 许可发布