“我在 Dockerfile 里 rm 了,镜像怎么没变小?”是最经典的镜像体积问题。答案在层的结构:每个 RUN 产生一层,删除只是在上面盖一层。这篇用 docker history、docker save 打开层的 tar 看到那个 0 字节的 whiteout 文件,并验证三种做法的体积差别。
alpine:3.20 的 docker images:IMAGE / ID / DISK USAGE / CONTENT SIZE / EXTRA | alpine:3.20 / 45e09956dc66 / 13.6MB / 4.09MB
[两个 RUN:先 dd,再 rm] 退出码 0;docker image inspect 的 Size = 55.54 MB(比 alpine 多 41.96 MB)
docker images:IMAGE / ID / DISK USAGE / CONTENT SIZE / EXTRA | bwlab-c3a:latest / 3c6b5ef18423 / 55.5MB / 25.1MB
[一个 RUN:dd && rm] 退出码 0;docker image inspect 的 Size = 13.58 MB(比 alpine 多 0.00 MB)
docker images:IMAGE / ID / DISK USAGE / CONTENT SIZE / EXTRA | bwlab-c3b:latest / 4ea1eb3e0d16 / 13.6MB / 4.1MB两个 RUN(先 dd、再 rm):docker history 里 dd 那层 21MB,rm 那层 4.1kB,镜像 DISK USAGE 55.5MB。同一个 RUN 里 dd && rm:只留下 4.1kB 的元数据层,镜像和 alpine 一样大。这个 Docker 的 Size 是压缩内容加解包后的层,随机数据压不动,所以 20 MiB 的层被算了两遍。
bwlab-c3a:
4.1kB RUN /bin/sh -c rm /big.bin # buildkit
21MB RUN /bin/sh -c dd if=/dev/urandom of=/big.bi…
0B CMD ["/bin/sh"]
9.49MB ADD alpine-minirootfs-3.20.10-aarch64.tar.gz…
bwlab-c3b:
4.1kB RUN /bin/sh -c dd if=/dev/urandom of=/big.bi…
0B CMD ["/bin/sh"]
9.49MB ADD alpine-minirootfs-3.20.10-aarch64.tar.gz…docker save 导出后,dd 那一层的 tar 里有 big.bin(20971520 字节),rm 那一层只有一个 0 字节的 .wh.big.bin。叠加文件系统看到 .wh. 前缀的条目,就把下层同名的文件隐藏起来——文件还在,只是看不见。
bwlab-c3a:共 3 层(含 alpine 基础层),后面几层:
层 63b6b9215867… 文件大小 20.98 MB;条目 1 个:big.bin(20971520B)
层 e5fc68998cee… 文件大小 0.00 MB;条目 1 个:.wh.big.bin(0B)
bwlab-c3b:共 2 层(含 alpine 基础层),后面几层:
层 4f4fb700ef54… 文件大小 0.00 MB;条目 0 个:把 Dockerfile 拆成两个阶段并单独构建到 gen 阶段,从那个镜像启动能 ls 到 20971520 字节的 /big.bin;最终镜像看不到它。所以任何曾经写进镜像的秘密或大文件,都会一直躺在某一层里,拿到镜像的人可以从层里取出来。
从 gen 阶段的镜像启动:ls -la /big.bin → -rw-r--r-- 1 root root 20971520 Oct 10 07:17 /big.bin 从最终镜像(多了 rm 那一层)启动:ls -la /big.bin → ls: /big.bin: No such file or directory
要么在同一个 RUN 里创建又删除(层里什么都不留),要么用多阶段构建把临时文件留在前一个阶段,只 COPY --from 需要的。后者的实验结果是 13.59 MB,比 alpine 只多 0.01 MB。
[多阶段:生成阶段用掉 20 MiB,最终阶段只 COPY --from 一个小文件] 镜像 13.59 MB(比 alpine 多 0.01 MB)
压缩算法(gzip/zstd)对层大小的影响、不同的镜像存储(经典 overlay2 与 containerd 快照)对 Size 口径的影响、docker build --squash(下面会有专门一篇)都没有在本文实验。