Docker 多阶段构建(Multi-stage Build)
核心概念
多阶段构建允许在同一个 Dockerfile 中使用多个 FROM 指令。每个 FROM 开始一个独立的构建阶段,你可以从前面阶段复制文件到后面的阶段——这样能只保留最终需要的文件,大幅缩小镜像体积。
# 第一阶段:构建环境(可能很大)
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
# 第二阶段:运行环境(非常小)
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
graph LR
subgraph 第一阶段: builder
GOLANG[golang:1.20<br/>800MB]
BUILD[编译 myapp]
end
subgraph 第二阶段: final
ALPINE[alpine:latest<br/>5MB]
COPY_BIN[复制编译好的二进制]
end
GOLANG --> BUILD -->|COPY --from=builder| COPY_BIN --> FINAL[最终镜像 10MB<br/>从 800MB→10MB!]
为什么需要多阶段构建?
传统构建的问题
# ❌ 单阶段构建 — 工具链留在镜像中
FROM golang:1.20
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
CMD ["/myapp"]
# 最终镜像
docker images myapp
# myapp latest abc123... 800MB ← 巨大的镜像!
# 其中 790MB 是 Go 编译工具链,运行完全不需要
多阶段构建的解决方案
# ✅ 多阶段构建 — 只复制需要的文件
FROM golang:1.20 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# 最终镜像
docker images myapp
# myapp latest def456... 10MB ← 小了 99%!
多阶段构建的完整示例
Go 应用
# ====== 阶段1:编译 ======
FROM golang:1.20 AS builder
WORKDIR /src
# 先复制依赖文件(利用缓存)
COPY go.mod go.sum ./
RUN go mod download
# 复制源码并编译
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/myapp
# ====== 阶段2:打包 ======
FROM alpine:3.18
# 安装运行时依赖(如果应用需要)
RUN apk add --no-cache ca-certificates tzdata
# 从 builder 阶段复制二进制
COPY --from=builder /app/myapp /myapp
# 设置运行用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
CMD ["/myapp"]
Node.js 应用
# ====== 阶段1:安装依赖和构建 ======
FROM node:18-alpine AS builder
WORKDIR /build
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
RUN yarn build
# ====== 阶段2:生产运行 ======
FROM node:18-alpine
# 只复制生产依赖和构建产物
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/node_modules ./node_modules
COPY --from=builder /build/package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]
Python 应用
# ====== 阶段1:安装依赖 ======
FROM python:3.11-slim AS builder
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/ ./app/
# 确保 PATH 包含 user 安装的包
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app/main.py"]
多阶段构建的高级技巧
技巧一:条件构建(多平台)
# 根据不同架构选择不同的基础镜像
FROM --platform=$BUILDPLATFORM golang:1.20 AS builder
ARG TARGETOS
ARG TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o myapp
FROM alpine:latest
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# 构建多架构
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myapp:latest .
技巧二:使用多个源
# 从不同阶段获取不同文件
FROM alpine:latest AS base
RUN apk add --no-cache curl
FROM python:3.11-slim AS tools
RUN pip install --user awscli
FROM alpine:latest AS final
# 从 base 复制 curl
COPY --from=base /usr/bin/curl /usr/bin/curl
# 从 tools 复制 awscli
COPY --from=tools /root/.local /root/.local
技巧三:只在某一阶段保留调试工具
# 开发阶段:包含调试工具
FROM node:18-alpine AS development
RUN apk add --no-cache curl vim
WORKDIR /app
COPY . .
CMD ["npm", "run", "dev"]
# 生产阶段:精简
FROM node:18-alpine AS production
COPY --from=development /app/dist ./dist
COPY --from=development /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
# 构建不同版本
docker build --target development -t myapp:dev .
docker build --target production -t myapp:prod .
多阶段构建的缓存策略
graph TB
subgraph 构建流程
S1[Stage 1: golang:1.20<br/>缓存: ✅ 基础镜像]
S2[Stage 1: COPY go.mod<br/>缓存: ✅ 依赖没变]
S3[Stage 1: RUN go mod download<br/>缓存: ✅ 依赖没变]
S4[Stage 1: COPY . .<br/>缓存: ❌ 源码变了]
S5[Stage 1: RUN go build<br/>缓存: ❌ 需要重新编译]
S1 --> S2 --> S3 --> S4 --> S5
S6[Stage 2: FROM alpine:latest<br/>缓存: ✅ 基础镜像]
S7[Stage 2: COPY --from=builder<br/>缓存: ❌ 二进制变了]
S5 --> S6 --> S7
end
镜像大小对比
# 单阶段构建
golang:1.20 -> 800MB
+ 应用二进制 -> 10MB
总计: 810MB
# 多阶段构建
alpine:latest -> 5MB
+ 应用二进制 -> 10MB
+ ca-certificates -> 3MB
总计: 18MB
# 节省率: 97.8%!
常见错误
错误一:忘记复制必要的运行时文件
FROM golang:1.20 AS builder
COPY . .
RUN go build -o myapp
FROM alpine:latest
# ❌ 没有复制证书和时区
COPY --from=builder /app/myapp /myapp
CMD ["/myapp"]
# 运行时报错: x509: certificate signed by unknown authority
错误二:在最终阶段中仍然使用了完整的工具链
FROM node:18 AS builder
# ... 构建 ...
FROM node:18
# ❌ 又使用了完整版 node:18(1.1GB)
# 应该用 node:18-alpine (125MB)
FROM node:18-alpine AS final
COPY --from=builder /build/dist ./dist
# ✅ 小很多
错误三:没有利用依赖缓存
# ❌ 每次源码变更都重新下载依赖
FROM golang:1.20 AS builder
COPY . . # 源码复制在前
RUN go mod download # 源码变了,缓存失效,重新下载
RUN go build -o myapp
# ✅ 分离依赖和源码
FROM golang:1.20 AS builder
COPY go.mod go.sum ./ # 先复制依赖文件
RUN go mod download # 缓存最大化
COPY . .
RUN go build -o myapp
面试追问
问:多阶段构建最终的镜像包含了多少个阶段?
答:只包含最后一个阶段以及通过 COPY --from= 从前面阶段复制的文件。前面阶段的基础镜像和层不会存在于最终镜像中。
问:可以在一个 Dockerfile 中用超过两个阶段吗?
答:可以。可以按需使用任意数量的阶段,通常用于:
– 阶段1:编译/构建
– 阶段2:安装测试依赖并运行测试
– 阶段3:生成精简的生产镜像
总结
| 对比项 | 单阶段构建 | 多阶段构建 |
|---|---|---|
| 最终镜像大小 | 大(包含工具链) | 小(只包含所需) |
| 安全性 | 低(包含多余工具) | 高(减少攻击面) |
| 构建复杂度 | 低 | 中(需要设计阶段分离) |
| 构建速度 | 中 | 好(缓存利用率高) |
| 推荐 | ❌ | ✅ 必须使用 |
一句话总结:多阶段构建是「把大象装冰箱」——在第一个冰箱里装进大象,然后只把需要的肉拿出来放进第二个小冰箱里,大冰箱就可以扔了。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END


暂无评论内容