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 许可发布