多阶段构建详解
多阶段构建是 Docker 17.05 引入的强大特性,允许在单个 Dockerfile 中使用多个 FROM 语句。这种构建方式可以显著减小最终镜像体积,同时保持构建环境的完整性。
目录
多阶段构建基础
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"]
# 镜像大小: ~20MB1.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"]
# 镜像大小: ~5MB3.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 GB | 15 MB | 98.8% |
| Node.js 应用 | 1.1 GB | 180 MB | 83.6% |
| Python 应用 | 1.3 GB | 150 MB | 88.5% |
| Java 应用 | 800 MB | 220 MB | 72.5% |
| Rust 应用 | 2.0 GB | 25 MB | 98.8% |
下一步
- 学习 Docker 镜像构建最佳实践
- 了解 Docker 安全实践
- 掌握 容器编排入门