Skip to content

第 7 章 多阶段构建与镜像瘦身 ​

本章目标:理解多阶段构建的机制与收益,掌握镜像瘦身的常用手段(alpine、多阶段、层合并),并能用构建前后体积对比验证效果。

1. 为什么要给镜像瘦身 ​

镜像体积直接影响三件事:

  • 传输与启动:拉取时间、CI/CD 推送时间、冷启动时间;
  • 存储成本:镜像数量 × 体积;
  • 安全攻击面:镜像里多装的每一个包都是潜在漏洞入口。

"胖镜像"的典型成因:构建工具与运行产物混在一个镜像里——比如一个 Go/Python/C 项目,编译或打包需要 GCC、pip 缓存、源码,这些最终运行时根本用不到,却全被塞进了交付镜像。

2. 多阶段构建:构建环境与运行环境分离 ​

一个 Dockerfile 里可以有多个 FROM,每个 FROM 开启一个阶段;只有最后一个阶段会进入最终镜像,前面阶段只是构建期的中间产物。

dockerfile
# ===== 阶段 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"]

两个关键点:

  1. COPY --from=builder <源> <目标>:从别的阶段复制文件(而不是构建上下文);
  2. 阶段 1 的一切(Go 编译器、源码、依赖)都不会出现在最终镜像里——最终镜像只有 alpine + 编译好的二进制,体积可能从 800MB 降到 20MB。

配套命令:

bash
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 的索引缓存会留在镜像里):

dockerfile
# ✅ 一条 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)**只保留产物——构建工具不进最终镜像。所有镜像均已在本地,构建全程无需联网:

python
# 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")
dockerfile
# 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"]

构建并对比体积:

bash
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,镜像里只剩程序本身。

练习 ​

  1. 在 examples/07-multi-stage-builds/ 构建镜像,用 docker images 记录体积,再用 docker history 确认最终镜像里没有 python 构建层。
  2. 用配套的 Dockerfile.single 构建单阶段版本,对比两种写法的镜像体积差异(预期差 5 倍以上)。
  3. 执行 docker build --target builder -t hello-builder . 只构建阶段 1,用 docker images 对比 hello-builder 与 hello-multi 的体积,理解中间阶段与最终镜像的关系。
  4. 思考题:为什么多阶段构建里"把依赖安装放在源码复制之前"仍然重要?(提示:回顾第 6 章的层缓存。)

可运行示例见 examples/07-multi-stage-builds/,按其中的 README 步骤执行。

📖本文阅读--次|📊全站访问--次|👥访客--人