多阶段构建:如何生成数百 MB 镜像?
概述
多阶段构建(Multi-stage Build)是 Docker 17.05 引入的功能,允许在一个 Dockerfile 中使用多个 FROM 语句,每个 FROM 开始一个新的构建阶段。你可以从前一阶段复制文件到后一阶段,而前一阶段的所有中间产物(编译工具、依赖包等)都不会进入最终镜像。
旧时代的痛苦
没有多阶段构建之前
传统做法是使用多个 Dockerfile 或复杂脚本:
# 方案1:一个 Dockerfile,最终镜像巨大
FROM golang:1.21
WORKDIR /build
COPY . .
RUN go build -o myapp .
# 镜像包含 Go 编译器、编译工具链,>1GB
# 方案2:两个 Dockerfile,脚本串联(繁琐)
# Dockerfile.build
FROM golang:1.21 AS builder
WORKDIR /build
COPY . .
RUN go build -o myapp .
# Dockerfile.prod
FROM alpine:3.18
COPY --from=builder /build/myapp /myapp
# 需要外部构建流程来配合
flowchart LR
subgraph "传统方式(~1.2GB)"
A1[golang:1.21] --> A2[编译工具+依赖]
A2 --> A3[二进制作品]
A3 --> A4["最终镜像\n(包含编译器!)"]
end
subgraph "多阶段构建(~15MB)"
B1["阶段1: golang:1.21"] --> B2[编译]
B2 --> B3["阶段2: alpine:3.18"]
B3 --> B4["仅复制二进制\n最终镜像极小"]
end
多阶段构建实战
Go 应用示例
# === 阶段1:构建 ===
FROM golang:1.21 AS builder
WORKDIR /app
# 利用缓存:先复制依赖文件
COPY go.mod go.sum ./
RUN go mod download
# 构建二进制
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .
# === 阶段2:运行 ===
FROM alpine:3.18
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
CMD ["./server"]
docker build -t myapp:latest .
docker images myapp
# REPOSITORY TAG IMAGE ID SIZE
# myapp latest abc123def456 15.2MB
Node.js 应用示例
# === 阶段1:安装依赖和构建 ===
FROM node:18-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # 假设生成 dist/ 目录
# === 阶段2:生产运行 ===
FROM node:18-alpine
WORKDIR /app
# 只复制生产依赖和构建产物
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/package.json .
EXPOSE 3000
CMD ["node", "dist/server.js"]
Python 应用示例
# === 阶段1:构建依赖 ===
FROM python:3.11-slim AS builder
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential && \
rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY requirements.txt .
RUN pip install --user -r requirements.txt
# === 阶段2:运行 ===
FROM python:3.11-slim
COPY --from=builder /root/.local /root/.local
COPY . /app
WORKDIR /app
# 确保 local/bin 在 PATH 中
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]
高级技巧
技巧 1:命名阶段
# 可以给每个阶段命名,通过名字复制
FROM maven:3.9-eclipse-temurin-17 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:17-jre-alpine
COPY --from=builder /build/target/*.jar /app/app.jar
CMD ["java", "-jar", "/app/app.jar"]
技巧 2:多架构编译
FROM --platform=$BUILDPLATFORM golang:1.21 AS builder
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o server .
FROM alpine:3.18
COPY --from=builder /app/server /server
CMD ["/server"]
# 构建多架构镜像
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
技巧 3:只使用阶段中的部分
FROM ubuntu:22.04 AS base
RUN apt update && apt install -y curl # 可能很大
RUN echo "shared script" > /script.sh
FROM alpine:3.18
# 只复制脚本文件,跳过 curl
COPY --from=base /script.sh /script.sh
RUN cat /script.sh && rm /script.sh
CMD ["echo", "hello"]
技巧 4:利用构建目标停止
# 如果你只需要编译产物,不需要最终镜像
docker build --target builder -t myapp-builder .
# 构建在 builder 阶段结束后停止
镜像大小对比
flowchart TD
A[Go Web 应用] --> B[单阶段构建: golang:1.21]
A --> C[多阶段构建: alpine:3.18]
A --> D[多阶段构建: scratch]
B --> B1["~1.2GB"]
C --> C1["~15MB"]
D --> D1["~8MB"]
C1 --> E["减少 98.7%"]
D1 --> F["减少 99.3%"]
style B1 fill:#ffcdd2
style C1 fill:#c8e6c9
style D1 fill:#c8e6c9
| 应用类型 | 单阶段 | 多阶段(Alpine) | 多阶段(Scratch) |
|---|---|---|---|
| Go 二进制 | 1.2 GB | 15 MB | 8 MB |
| Java Spring | 700 MB | 180 MB | —— |
| Node.js 应用 | 650 MB | 180 MB | —— |
| Python 应用 | 500 MB | 150 MB | —— |
最佳实践
| 实践 | 说明 |
|---|---|
| 最小运行镜像 | 只用 scratch、alpine 或 distroless 作为运行阶段 |
| 阶段命名 | 每个阶段用 AS name 命名,方便复制引用 |
| 利用缓存 | 第一阶段也分层写,先复制依赖后复制源码 |
| 不重复安装 | 工具只在一个阶段安装,不要跨阶段复制 |
| 构建时依赖 vs 运行时依赖 | 编译工具只在构建阶段,运行时镜像只需要运行库 |
使用 --target 调试 |
开发时构建到特定阶段,快速调试 |
注意 COPY --from 路径 |
路径是相对于前一阶段的工作目录 |
总结
- 多阶段构建 = 多个
FROM+COPY --from - 第一阶段负责编译、构建、打包
- 第二阶段(最终阶段)只包含运行所需的最小内容
- 最终镜像大小可减少 90%-99%
- 支持构建时跳过不必要的阶段(
--target) - 每个阶段独立缓存,提高构建速度
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容