bigywater.tech
Docker · 8

删除文件为什么不减小镜像:whiteout 文件与层

“我在 Dockerfile 里 rm 了,镜像怎么没变小?”是最经典的镜像体积问题。答案在层的结构:每个 RUN 产生一层,删除只是在上面盖一层。这篇用 docker history、docker save 打开层的 tar 看到那个 0 字节的 whiteout 文件,并验证三种做法的体积差别。

+41.96 MB
20 MiB 的随机文件先创建、下一层再删除后,镜像比基础镜像多出的 Size
.wh.big.bin
删除那一层里唯一的条目:0 字节的 whiteout 文件
+0.01 MB
多阶段构建只 COPY --from 一个小文件后,最终镜像比 alpine 多出的体积

分两层与合并一层

实测 · 本机执行输出
  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 的层被算了两遍。

whiteout 是什么

实测 · 本机执行输出
  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(下面会有专门一篇)都没有在本文实验。

分步演示

层与 whiteout
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Docker Desktop(Engine 29.8.2,容器跑在 Docker Desktop 自带的 Linux 虚拟机里,内核与宿主机的 macOS 不同);镜像为 linux/arm64;需要看虚拟机内部时用特权容器 + nsenter;计时用 time / date +%s%N,数字每次运行会有小幅抖动。随机文件由 dd if=/dev/urandom 生成;层的内容通过 docker save 导出的 tar 用 Python tarfile 解析;大小口径用 docker image inspect 的 Size 与 docker images 的 DISK USAGE / CONTENT SIZE;每种情形各一次;实验后已删除所有镜像
    • 没有验证:经典 overlay2 镜像存储下的 Size 口径(本机是 containerd 镜像存储)、zstd 压缩的层、OCI 与 Docker 格式 whiteout 的细微差别(opaque 目录在后面的文章)、镜像仓库里压缩后的真实传输大小
    京ICP备2026067502号-1京公网安备11010502063487号