Docker 多阶段构建
本章将深入讲解 Docker 多阶段构建技术,这是一种强大的镜像优化技术,可以显著减少最终镜像的大小并提高安全性。
多阶段构建概述
什么是多阶段构建?
多阶段构建允许在一个 Dockerfile 中使用多个 FROM 指令,每个 FROM 指令开始一个新的构建阶段。你可以选择性地将文件从一个阶段复制到另一个阶段,在最终镜像中只保留需要的内容。
多阶段构建的优势
- 减小镜像大小:排除构建工具和中间文件
- 提高安全性:减少攻击面,不包含构建工具
- 简化部署:单个 Dockerfile 管理整个构建流程
- 提高构建效率:并行构建和缓存优化
- 环境一致性:确保构建和运行环境的一致性
传统构建 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}'最佳实践总结
设计原则
- 最小化最终镜像:只包含运行时必需的文件
- 合理分阶段:根据功能和依赖关系划分阶段
- 优化缓存:将变化频率低的操作放在前面
- 并行构建:利用独立阶段提高构建效率
- 安全考虑:不在最终镜像中包含构建工具
命名规范
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 镜像。