3.4 多阶段构建与镜像瘦身


3.4 多阶段构建与镜像瘦身

本节摘要:同一个应用,随手装箱几百 MB,用心装箱几十 MB——差距全在"生产废料有没有跟船"。本节讲多阶段构建的核心思路与一组可执行的瘦身手法,并用前后实验验证效果。

箱子里混进了生产废料

装箱单会写了,看一个真实痛点:一个几 MB 的 Go 程序,装出来的镜像却有上 GB;一个 Python 应用,镜像里带着编译工具链和下载缓存。原因在 3.1 的层规则里早就埋好了——层是叠加的,删除文件的指令不会让下面的层变小,只会多一层"删除记录"

看一个典型反例,找出它为什么瘦不下来:

# 反例:单阶段构建,废料全在箱里 FROM golang:1.22 # 底箱:完整 Go 工具链,约 800MB WORKDIR /build COPY . . RUN go build -o server . # 编译出十几 MB 的成品 RUN rm -rf /build /go/pkg # 事后清理:删了也白删! # rm 只是给可叠加的层增加"删除记录",下面层的字节数原封不动 CMD ["./server"]

rm 删不掉上一层的字节数——这是新手期最反直觉的一条规则。瘦身不能靠"事后打扫",要靠装箱流程本身的分阶段

多阶段构建:车间里出成品,终箱只装成品

多阶段构建的思路用码头话说就是:装箱流程分成车间段与终箱段。车间段用带全套工具链的重底箱编译出成品;终箱段换一只干净轻便的底箱,只从车间段把成品文件接过来。编译器、源码、包缓存统统留在车间,物理上进不了终箱。

# 多阶段构建:车间段 + 终箱段 # ---------- 车间段:负责编译 ---------- FROM golang:1.22 AS builder # 给本阶段起名 builder WORKDIR /build COPY go.mod go.sum ./ RUN go mod download # 依赖下载单独成层,吃缓存红利 COPY . . RUN go build -o server . # 成品诞生,废料也诞生在这里 # ---------- 终箱段:只装成品 ---------- FROM alpine:3.20 # 换极简底箱,约 5MB RUN apk add --no-cache ca-certificates # 成品需要的运行时依赖才安装 COPY --from=builder /build/server /usr/local/bin/server # 只接成品,不接车间 CMD ["server"]

COPY --from=builder 是关键一笔:跨阶段复制,只把成品文件搬进终箱。跑一遍看看体积对比:

# 构建多阶段镜像 docker build -t myserver:slim . # 同一应用两种装法的体积对比 docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" | grep -E "myserver|REPO" # REPOSITORY TAG SIZE # myserver fat 1.02GB <- 单阶段反例 # myserver slim 18.7MB <- 多阶段正例:缩到五十分之一

图 3-2:单阶段与多阶段构建的体积与内容对比

图 3-2:单阶段与多阶段构建的体积与内容对比

解释型语言也要多阶段吗

Go 的例子最极端,Python 这类解释型语言同样受益,只是"废料"换成了 wheel 包与编译头文件。看一个把依赖准备拆进车间段的写法:

# Python 多阶段:车间段备料,终箱段只装成品 FROM python:3.12-slim AS wheels WORKDIR /w RUN pip wheel --no-deps -w /wheels flask==3.0.3 gunicorn==22.0.0 # 车间段产出 wheel 安装包,编译工具链留在车间 FROM python:3.12-slim COPY --from=wheels /wheels /wheels RUN pip install --no-cache-dir /wheels/*.whl && rm -rf /wheels # 安装介质在同一条指令里清掉,终箱只留装好的包 COPY app/ /app/ CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]

纯 Python 依赖的收益有限,但项目一旦出现需要现场编译的扩展(数据库驱动、加密库),这套分层能让上百 MB 的编译工具链彻底不进终箱。判断标准一句话:装箱过程中用过、运行时用不到的东西,都该留在车间

瘦身手法清单

多阶段构建是主菜,配菜按收益排序:

一、选轻底箱。 终箱段的底座能小则小:完整发行版换 slim 通常省一个数量级,换 alpine 再省一截(留意 libc 兼容问题)。解释型语言同理——Python 的 slim 底箱配合虚拟环境是常用组合。

二、合并指令并在同一条里清理。 3.3 的规则在瘦身场景的完整形态:装完即清必须发生在同一条 RUN 里,跨指令清理无效。

# 正确:安装与清理同一条指令,缓存目录当场清空 RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/*

三、.dockerignore 挡废料于门外。 本地开发目录里的历史日志、临时文件、密钥样例,不挡就全进构建上下文,白白增大传输与误入镜像的风险。

四、依赖层与代码层分离。 既是缓存优化(3.3)也是瘦身手段:依赖清单单独成层,代码变动不会引发依赖层重建与重新下载。

瘦身的收益不止体积:镜像小,提货快、部署快、攻击面小(终箱里连 shell 都没有的极简镜像,入侵者拿到手也难以横移),堆场存储成本同步下降。体积是镜像质量最诚实的单一指标。

底座的选择上再多说一句取舍。alpine 用的是另一套 C 标准库实现(musl),绝大多数场景无感,但个别依赖动态链接 glibc 的程序会出兼容问题;另一条路线是 distroless 类"只放运行时"的底座,体积与安全面更小,代价是完全没有 shell、排查全靠旁路手段(4.3 节讲过应对)。选型建议从 slim 起步,验证稳定后再评估要不要下探到更小的底座——瘦身是手段不是目的,稳定性红线优先于体积数字

本节要点回顾

  • 层不可变改写:删除指令只加记录不减体积,事后打扫无效
  • 多阶段构建:车间段编译、终箱段只收成品,COPY --from 是接成品的唯一通道。
  • 实测收益:同一 Go 应用从 1.02GB 降到 18.7MB,提货部署速度同步提升。
  • 配菜四项:轻底箱、同条指令内清理、.dockerignore 挡门、依赖层与代码层分离。
  • 极简镜像连 shell 都没有:体积、速度、安全三线收益,瘦身是专业分界线。

你的箱子已经又小又干净。最后一站:给它建一座不依赖外网的专属堆场。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U