Skip to content

Docker 多阶段构建

本章将深入讲解 Docker 多阶段构建技术,这是一种强大的镜像优化技术,可以显著减少最终镜像的大小并提高安全性。

多阶段构建概述

什么是多阶段构建?

多阶段构建允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令开始一个新的构建阶段。你可以选择性地将文件从一个阶段复制到另一个阶段,在最终镜像中只保留需要的内容。

多阶段构建的优势

  1. 减小镜像大小:排除构建工具和中间文件
  2. 提高安全性:减少攻击面,不包含构建工具
  3. 简化部署:单个 Dockerfile 管理整个构建流程
  4. 提高构建效率:并行构建和缓存优化
  5. 环境一致性:确保构建和运行环境的一致性

传统构建 vs 多阶段构建

传统构建问题:

dockerfile
# 传统方式 - 镜像包含所有构建工具
FROM node:16
WORKDIR /app
COPY package*.json ./
RUN npm install  # 包含开发依赖
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
# 结果:大镜像,包含不必要的构建工具和依赖

多阶段构建解决方案:

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

# 生产阶段
FROM node:16-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["npm", "start"]
# 结果:小镜像,只包含运行时需要的文件

基础多阶段构建

简单的两阶段构建

dockerfile
# 第一阶段:构建应用
FROM golang:1.19-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

# 第二阶段:运行时环境
FROM alpine:3.16

# 安装 CA 证书
RUN apk --no-cache add ca-certificates

WORKDIR /root/

# 从构建阶段复制二进制文件
COPY --from=builder /app/main .

EXPOSE 8080
CMD ["./main"]

命名构建阶段

dockerfile
# 使用有意义的阶段名称
FROM node:16 AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:16 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:16-alpine AS runtime
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
CMD ["npm", "start"]

高级多阶段构建模式

并行构建阶段

dockerfile
# 基础阶段
FROM node:16-alpine AS base
WORKDIR /app
COPY package*.json ./

# 依赖安装阶段
FROM base AS deps
RUN npm ci --only=production && npm cache clean --force

# 开发依赖阶段
FROM base AS dev-deps
RUN npm ci

# 构建阶段
FROM dev-deps AS build
COPY . .
RUN npm run build

# 测试阶段
FROM dev-deps AS test
COPY . .
RUN npm run test
RUN npm run lint

# 生产阶段
FROM base AS production
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

条件构建阶段

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

# 开发环境阶段
FROM base AS development
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]

# 构建阶段
FROM base AS build
RUN npm ci
COPY . .
RUN npm run build

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

# 测试阶段
FROM build AS test
RUN npm run test

特定语言的多阶段构建

Java 应用

dockerfile
# Maven 构建阶段
FROM maven:3.8-openjdk-11 AS build

WORKDIR /app
COPY pom.xml .
COPY src ./src

# 构建应用
RUN mvn clean package -DskipTests

# 运行时阶段
FROM openjdk:11-jre-slim

# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash app

WORKDIR /app

# 复制 JAR 文件
COPY --from=build --chown=app:app /app/target/*.jar app.jar

USER app

EXPOSE 8080

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

Python 应用

dockerfile
# 构建阶段
FROM python:3.9 AS builder

WORKDIR /app

# 安装构建依赖
RUN pip install --user pipenv

# 复制依赖文件
COPY Pipfile Pipfile.lock ./

# 安装 Python 依赖
RUN pipenv install --deploy --system --user

# 运行时阶段
FROM python:3.9-slim

# 复制 Python 包
COPY --from=builder /root/.local /root/.local

# 确保脚本在 PATH 中
ENV PATH=/root/.local/bin:$PATH

# 创建非 root 用户
RUN useradd --create-home --shell /bin/bash app

WORKDIR /app

# 复制应用代码
COPY --chown=app:app . .

USER app

EXPOSE 8000

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

.NET 应用

dockerfile
# SDK 镜像用于构建
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build

WORKDIR /src
COPY ["MyApp.csproj", "."]
RUN dotnet restore "MyApp.csproj"

COPY . .
RUN dotnet build "MyApp.csproj" -c Release -o /app/build

# 发布阶段
FROM build AS publish
RUN dotnet publish "MyApp.csproj" -c Release -o /app/publish

# 运行时镜像
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS runtime

WORKDIR /app
COPY --from=publish /app/publish .

EXPOSE 80
ENTRYPOINT ["dotnet", "MyApp.dll"]

React 应用

dockerfile
# 构建阶段
FROM node:16-alpine AS build

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

COPY . .
RUN npm run build

# 生产阶段 - 使用 Nginx 服务静态文件
FROM nginx:alpine AS production

# 复制构建产物
COPY --from=build /app/build /usr/share/nginx/html

# 复制 Nginx 配置
COPY nginx.conf /etc/nginx/nginx.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

优化技巧

缓存优化

dockerfile
# 优化构建缓存
FROM node:16-alpine AS base

WORKDIR /app

# 1. 先复制依赖文件(变化频率低)
COPY package*.json ./

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

FROM base AS build-deps
RUN npm ci

FROM build-deps AS build
# 2. 后复制源代码(变化频率高)
COPY . .
RUN npm run build

FROM base AS runtime
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package*.json ./
CMD ["npm", "start"]

并行构建优化

dockerfile
FROM alpine:3.16 AS base
RUN apk add --no-cache ca-certificates

# 并行阶段 1:下载依赖
FROM base AS downloader
RUN apk add --no-cache curl
RUN curl -L https://example.com/file1.tar.gz -o /tmp/file1.tar.gz
RUN curl -L https://example.com/file2.tar.gz -o /tmp/file2.tar.gz

# 并行阶段 2:编译工具
FROM base AS compiler
RUN apk add --no-cache gcc musl-dev
COPY src/ /src/
RUN gcc -o /tmp/app /src/main.c

# 最终阶段:合并结果
FROM base AS final
COPY --from=downloader /tmp/file1.tar.gz /app/
COPY --from=downloader /tmp/file2.tar.gz /app/
COPY --from=compiler /tmp/app /usr/local/bin/
CMD ["/usr/local/bin/app"]

跨平台构建

dockerfile
# 支持多架构的多阶段构建
FROM --platform=$BUILDPLATFORM golang:1.19-alpine AS builder

ARG TARGETPLATFORM
ARG BUILDPLATFORM
ARG TARGETOS
ARG TARGETARCH

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o main .

FROM alpine:3.16
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/main /usr/local/bin/
CMD ["main"]

构建策略

选择性构建

bash
# 只构建到特定阶段
docker build --target build -t myapp:build .
docker build --target test -t myapp:test .
docker build --target production -t myapp:prod .

# 构建脚本示例
#!/bin/bash
STAGE=${1:-production}
VERSION=${2:-latest}

case $STAGE in
  "dev")
    docker build --target development -t myapp:dev .
    ;;
  "test")
    docker build --target test -t myapp:test .
    docker run --rm myapp:test
    ;;
  "prod")
    docker build --target production -t myapp:$VERSION .
    ;;
  *)
    echo "Usage: $0 {dev|test|prod} [version]"
    exit 1
    ;;
esac

构建参数传递

dockerfile
FROM node:16-alpine AS base

ARG NODE_ENV=production
ARG API_URL
ARG BUILD_VERSION

ENV NODE_ENV=$NODE_ENV
ENV API_URL=$API_URL
ENV BUILD_VERSION=$BUILD_VERSION

WORKDIR /app
COPY package*.json ./

FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

FROM base AS production
RUN npm ci --only=production
COPY . .
RUN npm run build
CMD ["npm", "start"]
bash
# 传递构建参数
docker build \
  --target production \
  --build-arg NODE_ENV=production \
  --build-arg API_URL=https://api.prod.com \
  --build-arg BUILD_VERSION=1.0.0 \
  -t myapp:1.0.0 .

实际应用案例

微服务应用

dockerfile
# 共享基础阶段
FROM node:16-alpine AS base
WORKDIR /app
RUN apk add --no-cache dumb-init
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force

# 用户服务
FROM base AS user-service
COPY services/user/ ./
EXPOSE 3001
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "index.js"]

# 订单服务
FROM base AS order-service
COPY services/order/ ./
EXPOSE 3002
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "index.js"]

# API 网关
FROM base AS api-gateway
COPY gateway/ ./
EXPOSE 3000
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "index.js"]

全栈应用

dockerfile
# 前端构建
FROM node:16-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ ./
RUN npm run build

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

# Nginx 配置
FROM nginx:alpine AS nginx-config
COPY nginx.conf /etc/nginx/nginx.conf

# 最终镜像
FROM nginx:alpine
COPY --from=nginx-config /etc/nginx/nginx.conf /etc/nginx/nginx.conf
COPY --from=frontend-build /app/frontend/dist /usr/share/nginx/html
COPY --from=backend-build /app/backend /app/backend

# 启动脚本
COPY start.sh /start.sh
RUN chmod +x /start.sh

EXPOSE 80
CMD ["/start.sh"]

调试和故障排查

调试多阶段构建

bash
# 查看构建历史
docker history myapp:latest

# 构建特定阶段进行调试
docker build --target build -t debug:build .
docker run -it debug:build /bin/sh

# 查看中间阶段
docker build --target builder -t myapp:builder .
docker run -it myapp:builder ls -la /app

# 使用 dive 分析镜像层
dive myapp:latest

常见问题解决

dockerfile
# 问题 1: 文件权限问题
FROM node:16-alpine AS builder
WORKDIR /app
COPY --chown=node:node . .
USER node
RUN npm run build

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

# 问题 2: 路径问题
FROM golang:1.19 AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/main .

FROM alpine:3.16
COPY --from=builder /app/main /usr/local/bin/
CMD ["main"]

# 问题 3: 环境变量传递
FROM node:16 AS builder
ARG API_URL
ENV REACT_APP_API_URL=$API_URL
COPY . .
RUN npm run build

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

性能监控

构建时间分析

bash
# 启用 BuildKit 详细输出
DOCKER_BUILDKIT=1 docker build --progress=plain -t myapp .

# 分析构建时间
time docker build -t myapp .

# 并行构建测试
docker build --target deps -t myapp:deps . &
docker build --target build -t myapp:build . &
wait

镜像大小对比

bash
#!/bin/bash
# compare-sizes.sh

echo "构建传统镜像..."
docker build -f Dockerfile.traditional -t myapp:traditional .

echo "构建多阶段镜像..."
docker build -f Dockerfile.multistage -t myapp:multistage .

echo "镜像大小对比:"
docker images | grep myapp | awk '{print $1":"$2, $7}'

最佳实践总结

设计原则

  1. 最小化最终镜像:只包含运行时必需的文件
  2. 合理分阶段:根据功能和依赖关系划分阶段
  3. 优化缓存:将变化频率低的操作放在前面
  4. 并行构建:利用独立阶段提高构建效率
  5. 安全考虑:不在最终镜像中包含构建工具

命名规范

dockerfile
# 使用描述性的阶段名称
FROM node:16 AS dependencies      # 依赖安装
FROM node:16 AS build            # 应用构建
FROM node:16 AS test             # 测试执行
FROM nginx:alpine AS production  # 生产运行

文档化

dockerfile
# 在 Dockerfile 中添加注释说明每个阶段的用途
# 第一阶段:安装构建依赖和工具
FROM node:16 AS builder
# 安装构建时需要的依赖...

# 第二阶段:构建应用
FROM builder AS build
# 编译和打包应用...

# 第三阶段:生产运行时环境
FROM node:16-alpine AS production
# 只复制运行时需要的文件...

本章小结

多阶段构建是 Docker 的一个强大特性,它能够:

关键优势:

  • 显著减小镜像大小:排除构建工具和中间文件
  • 提高安全性:减少攻击面
  • 简化构建流程:单个 Dockerfile 管理复杂构建
  • 提高构建效率:并行构建和缓存优化

最佳实践:

  • 合理设计构建阶段
  • 优化构建缓存策略
  • 使用描述性的阶段名称
  • 实施安全配置
  • 监控构建性能

应用场景:

  • 编译型语言应用(Go、Java、C++)
  • 前端应用构建(React、Vue、Angular)
  • 微服务架构
  • 复杂的全栈应用

掌握多阶段构建技术将帮助你构建更小、更安全、更高效的 Docker 镜像。

延伸阅读

本站内容仅供学习和研究使用。