第 7 章 多阶段构建与镜像瘦身
本章目标:理解多阶段构建的机制与收益,掌握镜像瘦身的常用手段(alpine、多阶段、层合并),并能用构建前后体积对比验证效果。
1. 为什么要给镜像瘦身
镜像体积直接影响三件事:
- 传输与启动:拉取时间、CI/CD 推送时间、冷启动时间;
- 存储成本:镜像数量 × 体积;
- 安全攻击面:镜像里多装的每一个包都是潜在漏洞入口。
"胖镜像"的典型成因:构建工具与运行产物混在一个镜像里——比如一个 Go/Python/C 项目,编译或打包需要 GCC、pip 缓存、源码,这些最终运行时根本用不到,却全被塞进了交付镜像。
2. 多阶段构建:构建环境与运行环境分离
一个 Dockerfile 里可以有多个 FROM,每个 FROM 开启一个阶段;只有最后一个阶段会进入最终镜像,前面阶段只是构建期的中间产物。
# ===== 阶段 1(builder):负责"造"产物,用什么工具都行 =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o /app/bin .
# ===== 阶段 2(最终):只要产物,不要构建工具 =====
FROM alpine:latest
COPY --from=builder /app/bin /usr/local/bin/myapp
CMD ["myapp"]两个关键点:
COPY --from=builder <源> <目标>:从别的阶段复制文件(而不是构建上下文);- 阶段 1 的一切(Go 编译器、源码、依赖)都不会出现在最终镜像里——最终镜像只有
alpine + 编译好的二进制,体积可能从 800MB 降到 20MB。
配套命令:
docker build -t myapp . # 完整构建
docker build --target builder -t myapp-builder . # 只构建到 builder 阶段(调试/CI 缓存用)
docker history myapp # 对比:最终镜像里只有阶段 2 的层阶段 1 还承担了"隔离构建环境"的作用:build 需要什么就装什么,不会污染运行环境。
3. 再配合其它瘦身手段
| 手段 | 做法 | 收益 |
|---|---|---|
| 多阶段构建 | 构建产物复制进精简运行镜像 | 去掉全部构建工具(最大头) |
| alpine 基础镜像 | python:3.12-alpine 而非 python:3.12 | 体积约差 5–10 倍(自带 musl + busybox) |
| 层合并 | 相关指令合并为一条 RUN(apk add 与清理缓存放同一层) | 减少层数与中间态残留 |
| 清理包管理器缓存 | apk add --no-cache ... 或装完即 rm -rf /var/cache/* | 去掉缓存文件 |
| .dockerignore | 排除 node_modules、.git、日志等 | 减小构建上下文传输 |
| 用编译型语言静态链接 | Go CGO_ENABLED=0、C 静态编译 | 产物直接跑在 scratch/alpine 上 |
层合并示例(否则 apk add 的索引缓存会留在镜像里):
# ✅ 一条 RUN 完成"装包 + 清理",缓存不落地
RUN apk add --no-cache curl && rm -rf /var/cache/apk/*scratch 空镜像:最终阶段甚至可以 FROM scratch(零基础镜像),只 COPY 一个静态二进制——镜像里只有你的程序。适合 Go/C 静态编译产物。
4. 完整示例:多阶段构建(本机可复现)
配套 examples/07-multi-stage-builds/。阶段 1 用 **python 镜像(约 74MB)**生成发布产物,阶段 2 用 **alpine(约 13MB)**只保留产物——构建工具不进最终镜像。所有镜像均已在本地,构建全程无需联网:
# examples/07-multi-stage-builds/gen.py:阶段 1 生成"发布产物"
import os
os.makedirs("/src/dist", exist_ok=True)
with open("/src/dist/release.txt", "w") as f:
f.write("Hello from multi-stage build! (artifact built in python stage)\n")# examples/07-multi-stage-builds/Dockerfile
# 阶段 1(builder):构建环境,工具随便装(python 镜像约 74MB)
FROM python:3.12-alpine AS builder
WORKDIR /src
COPY gen.py .
RUN python gen.py
# 阶段 2(最终):只要产物,不要构建工具(alpine 约 13MB)
FROM alpine:latest
WORKDIR /app
COPY --from=builder /src/dist /app
CMD ["sh", "-c", "cat /app/release.txt"]构建并对比体积:
cd examples/07-multi-stage-builds
docker build -t hello-multi .
docker run --rm hello-multi # 输出 Hello from multi-stage build! ...
docker images hello-multi # 体积 ≈ 13MB(只有 alpine + 产物)
docker history hello-multi # 最终镜像只有阶段 2 的层
# 单阶段对比:python 镜像整个留在最终镜像里
docker build -t hello-fat -f Dockerfile.single .
docker images hello-fat # 体积 ≈ 74MB(比多阶段大 5 倍)结论:多阶段与单阶段产出同样的运行结果,体积却差 5 倍以上——差距全部来自"构建工具是否被带进了最终镜像"。真实项目中阶段 1 换成 gcc、go build、npm build 等编译打包工具,原理完全一致。
阶段 1 也可以不用
AS命名,但COPY --from=阶段名依赖名字,务必命名。
常见误区
- 误区:多阶段构建 = 多个 Dockerfile。 正确:一个 Dockerfile 内多个
FROM,只有最后一段进最终镜像。 - 误区:所有阶段都会进最终镜像。 正确:只有最后一个阶段;
COPY --from=只是把前面阶段的文件搬过来。 - 误区:alpine 一定是最优基础镜像。 正确:alpine 用了 musl libc,个别依赖 glibc 的程序(如某些二进制包)会不兼容;此时选 Debian slim 系(如
python:3.12-slim)更稳。 - 误区:瘦身只靠换基础镜像。 正确:最大头通常是构建工具残留,多阶段构建才是关键;基础镜像优化是锦上添花。
小结
- 多阶段构建:多个
FROM,COPY --from=builder取产物,最终镜像只含最后一个阶段。 - 瘦身三板斧:多阶段构建(去构建工具)→ alpine/slim 基础镜像(去系统冗余)→ 层合并与清理缓存(去残留)。
--target 阶段名可只构建到某阶段;docker history验证最终镜像的层。- 静态编译产物可
FROM scratch,镜像里只剩程序本身。
练习
- 在
examples/07-multi-stage-builds/构建镜像,用docker images记录体积,再用docker history确认最终镜像里没有 python 构建层。 - 用配套的
Dockerfile.single构建单阶段版本,对比两种写法的镜像体积差异(预期差 5 倍以上)。 - 执行
docker build --target builder -t hello-builder .只构建阶段 1,用docker images对比hello-builder与hello-multi的体积,理解中间阶段与最终镜像的关系。 - 思考题:为什么多阶段构建里"把依赖安装放在源码复制之前"仍然重要?(提示:回顾第 6 章的层缓存。)
可运行示例见
examples/07-multi-stage-builds/,按其中的 README 步骤执行。