这个系列一直没法拉基础镜像,连多阶段构建常见的 FROM golang AS builder 都用不了——干脆自己动手验证清楚 FROM scratch 能不能撑起一个真正的构建阶段:先证明 RUN 在没有 shell 的 scratch 阶段里也能跑(用 exec-form),再用它跑一个真实的"构建工具"生成产物,最后对比单阶段和多阶段两个真实构建出来的镜像,量出体积到底差多少。
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 阶段照样可以"运行一个程序、产出一个文件"。
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> 从它那里拿文件——只拿点名要的那几个路径,阶段里其它一切(工具、源码、中间产物)都不会跟着过来。这跟前一篇讲的"层缓存链"是两回事:多阶段构建关心的是"最终镜像里到底带不带某个文件",不是缓存命中与否。
同一个"构建工具" /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 buildtestbuildtest: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 到底带走了什么、扔下了什么。
数据直接对应上面真实实测的 buildtest:multi 构建过程和真实展开的层内容——tool 和 output.txt 都在 builder 阶段真实生成过,最终阶段真实只拿到了 output.txt。