bigywater.tech
Docker 系列 · 第五篇

多阶段构建里,"编译工具"真的会被完全丢在半路,一点都不带进最终镜像吗?

这个系列一直没法拉基础镜像,连多阶段构建常见的 FROM golang AS builder 都用不了——干脆自己动手验证清楚 FROM scratch 能不能撑起一个真正的构建阶段:先证明 RUN 在没有 shell 的 scratch 阶段里也能跑(用 exec-form),再用它跑一个真实的"构建工具"生成产物,最后对比单阶段和多阶段两个真实构建出来的镜像,量出体积到底差多少。

3.71MB → 11kB
本机真实实测:单阶段镜像(带着构建工具)对比多阶段镜像(只留产物),体积差了三百多倍
0 字节工具残留
本机真实实测:把多阶段镜像 docker save 展开,层里真实只有产物文件,构建工具一个字节都没进去

真实实测:FROM scratch 的阶段里,RUN 也能跑——只要不需要 shell

RUN 平时写的 RUN apt install ... 这种是"shell 形式",背后是 /bin/sh -c "..."——scratch 里没有 shell,这种写法必然失败。但 RUN 还有一种"exec 形式",直接执行一个可执行文件,不经过 shell。

真实实测
FROM scratch
COPY tool /tool
RUN ["/tool", "--build"]

$ docker build -f Dockerfile.exectest -t exectest:scratch .
#5 [2/2] RUN ["/tool", "--build"] #5 0.100 tool 正在运行,参数: [--build] #5 DONE 0.1s

构建日志里真实打印出了 /tool 自己的输出——它在构建过程中被真实执行了一次,而且真的往镜像文件系统里写了一个 /output.txt 文件。这条 exec-form 的 RUN 是撑起后面整个实验的关键:不需要任何基础镜像里的 shell,scratch 阶段照样可以"运行一个程序、产出一个文件"。

真实文档:多阶段构建的两个关键字——FROM ... AS 和 COPY --from

Docker 官方文档 · Dockerfile reference · docs.docker.com/reference/dockerfile To copy from a build stage in a multi-stage build, specify the name of the stage you want to copy from.

FROM <image> AS <name> 给某一个构建阶段起个名字,后面任意一个阶段都可以用 COPY --from=<name> 从它那里拿文件——只拿点名要的那几个路径,阶段里其它一切(工具、源码、中间产物)都不会跟着过来。这跟前一篇讲的"层缓存链"是两回事:多阶段构建关心的是"最终镜像里到底带不带某个文件",不是缓存命中与否。

真实实测:单阶段 vs 多阶段,镜像体积真实差了三百多倍

同一个"构建工具" /tool,单阶段版本让它留在最终镜像里;多阶段版本只 COPY --from=builder 它生成的 /output.txt。

真实实测
# 单阶段:工具本身也在最终镜像里
FROM scratch
COPY tool /tool
RUN ["/tool", "--build"]
ENTRYPOINT ["/tool"]

# 多阶段:只把产物抽出来
FROM scratch AS builder
COPY tool /tool
RUN ["/tool", "--build"]

FROM scratch
COPY --from=builder /output.txt /output.txt

$ docker images | grep buildtest
buildtest:multi 11kB 2.85kB buildtest:single 3.71MB 1.34MB

docker save 导出的真实 tar 包大小更直观:单阶段 1,352,704 字节,多阶段 13,824 字节——差了将近 100 倍。这个 /tool 只有 2.3MB 出头(实际项目里的编译工具链动辄几百 MB 到几 GB),差距已经这么明显,真实项目里省下来的体积只会更夸张。

真实实测:把最终镜像"拆开"看,工具真的一个字节都没进去

不满足于看总大小,直接把镜像导出来,看真正的层 tar 里装了什么。

真实实测
$ docker save buildtest:multi -o multi.tar
$ tar -xf multi.tar -C multi_inspect
$ for f in multi_inspect/blobs/sha256/*; do
    tar -tvf "$f" 2>/dev/null && echo "^^^ $f"
  done
-rw-r--r-- 0 0 0 57 Sep 20 11:35 output.txt ^^^ multi_inspect/blobs/sha256/dcdca3c8...

翻遍多阶段镜像里所有的层 blob,真正带文件内容的只有这一层,里面只有一个 57 字节的 output.txt——/tool 这个 2.3MB 的"编译工具"从头到尾没有出现在最终镜像的任何一层里。builder 阶段发生的一切(拷贝工具、真实执行、产出文件),对最终镜像来说,只有 COPY --from=builder 那一行点名要的文件才算数,其余的全部留在构建过程中,不会被打进最后交付的镜像。

演示:同一次构建,两个阶段各自留下了什么

对应上面真实实验的 builder 阶段和最终阶段,看 COPY --from 到底带走了什么、扔下了什么。

多阶段构建 · 文件流向演示

builder 阶段

    最终阶段

      数据直接对应上面真实实测的 buildtest:multi 构建过程和真实展开的层内容——tool 和 output.txt 都在 builder 阶段真实生成过,最终阶段真实只拿到了 output.txt。

      参考与说明

      • 本文全部真实实验(exec-form RUN 在 FROM scratch 阶段真实执行、单阶段与多阶段镜像的真实体积对比、多阶段镜像 docker save 展开后层内容的真实核对)均在本机真实运行的 Docker Desktop(BuildKit)上完成,用的是跟前几篇一样的本机交叉编译静态二进制技巧,全程零网络镜像拉取,数据未做删改。
      • 行为引用(FROM ... AS 与 COPY --from 的多阶段构建语义)取自 Docker 官方文档 docs.docker.com/reference/dockerfile,已用上面的真实构建结果逐条验证过。
      • 演示动画的文件流向数据直接取自真实的构建产物和真实展开的层内容,没有额外模拟或编造的文件列表。
      • 没有涉及:真实项目里用 golang/node 这类完整语言工具链镜像做 builder 阶段的实际编译过程(受限于这台机器拉不了基础镜像)、--target 只构建到某个中间阶段就停下的用法、多个阶段之间用 COPY --from= 按下标而不是按名字引用的写法、BuildKit 对多阶段构建里不相关阶段的并行调度。
      ☕ 如果这篇文章帮到你,可以请作者喝杯咖啡 · 爱发电
      京ICP备2026067502号-1京公网安备11010502063487号