大型 Docker 开发镜像磁盘排查:containerd、缓存与容量规划
摘要:给出大型开发镜像的磁盘排查顺序,区分 containerd 镜像数据、snapshot、BuildKit cache、导出归档和源码构建输出。
@[toc]
当一个二十多 GiB 的开发镜像运行在 Docker Engine 29+ 新主机上时,磁盘问题往往比容器运行问题更早出现。
例如根分区只有约 80 GiB,却同时存在:
1 | 大型开发镜像 |
这时如果只看到:
1 | /dev/sdaX 90% |
然后立刻运行 docker system prune -a,很容易删掉重新构建代价很高的开发镜像。
更好的方式是先回答:
空间到底被哪一类数据占用了?
1. 第一层先看文件系统
1 | df -h / |
它回答的是:
1 | 整个根文件系统还剩多少空间? |
它不会告诉你 Docker 哪个对象在占空间。
如果镜像归档放在其他分区,还要分别看:
1 | df -h /path/to/archive |
大型镜像导出时,Docker 数据和归档文件可能位于不同文件系统,不能只检查一个路径。
2. 第二层看宿主机真实目录
containerd image store 环境下,至少检查:
1 | sudo du -sh /var/lib/containerd /var/lib/docker |
这回答的是宿主机目录实际占用。
Docker 当前文档说明,containerd image store 下的 image contents 和 container snapshots 默认在:
1 | /var/lib/containerd |
所以新 Docker 环境中只看 /var/lib/docker 已经不够。
3. 第三层看 Docker 逻辑对象
1 | docker system df |
需要更详细时:
1 | docker system df -v |
它让你从 Docker 对象视角看:
1 | Images |
du 和 docker system df 不要求数字完全一致。
一个是文件系统目录视角,一个是 Docker 对象和共享关系视角。
4. 构建过大镜像以后单独看 BuildKit cache
如果刚执行过大型 docker buildx build,再看:
1 | docker buildx du |
大型 SDK 构建经常产生很大的 BuildKit cache。
它和“最终镜像本身”不是同一个对象:
1 | Build Cache |
如果 cache 明确可回收,可以:
1 | docker builder prune |
这通常是大型镜像构建完成后的第一类清理对象。
5. 为什么 builder prune 不会把 50 GiB 镜像变成 27 GiB
如果当前 containerd image store 需要同时保存:
1 | image content |
那么它们属于最终镜像运行体系。
执行:
1 | docker builder prune |
只会处理 BuildKit cache。
所以即使清掉 20 GiB 构建缓存,最终镜像相关的几十 GiB 仍然可能存在。
这不是 prune 失败,而是你清理的是不同对象。
6. inactive 不等于“不需要”
大型开发镜像有一个危险特点:平时不一定有常驻容器。
所以 Docker 可能显示:
1 | ACTIVE 0 |
但这只说明现在没有容器使用它,不代表镜像应该删除。
一个 20+ GiB 开发镜像可能构建几个小时,甚至需要重新复制大型 SDK。
因此看到 inactive 后不要直接:
1 | docker system prune -a |
更合理的是先确定:
1 | 这个镜像是否是正式开发环境? |
7. image prune 和 system prune 的范围不同
较温和:
1 | docker image prune |
更激进:
1 | docker image prune -a |
范围更广:
1 | docker system prune -a |
越往后,自动判断“未使用”的对象范围越大。
对于普通 CI 临时环境,这可能很方便;对于手工制作的大型 SDK 镜像,应该更保守。
建议先:
1 | docker system df -v |
确认对象,再做针对性清理。
8. 不要直接删除 /var/lib/containerd
即使:
1 | sudo du -sh /var/lib/containerd |
显示几十 GiB,也不要执行:
1 | sudo rm -rf /var/lib/containerd/* |
containerd 的 content、metadata 和 snapshot 之间存在引用关系。
绕过 Docker/containerd 管理层手工删除,可能导致:
1 | 镜像 metadata 仍然存在 |
底层目录不是普通“缓存目录”。
清理应优先通过 Docker CLI 或明确的官方迁移流程完成。
9. 导出归档经常是被忽略的一大块
例如你为了迁移镜像,在:
1 | ~/images/ |
放了:
1 | cross-dev.tar |
如果原始 TAR 没删,可能同时多占:
1 | 20+ GiB TAR |
它们完全不属于 Docker,因此:
1 | docker system df |
不会显示。
这就是为什么排查不能只看 Docker CLI。
可以检查:
1 | du -sh ~/images |
大型镜像场景里,“导出文件忘了删”非常常见。
10. 源码 build 目录也可能很大
如果开发容器使用 bind mount:
1 | 宿主机源码 -> /workspace |
那么容器生成的:
1 | build/ |
都可能实际写在宿主机项目目录。
这些也不属于 Docker image store。
所以完整排查还应该看:
1 | du -sh ~/work/demo-app |
不要看到“是容器编译产生的”就认为一定在 /var/lib/docker。
11. data-root 迁走后为什么根分区还可能增长
Docker 常见配置:
1 | { |
但 Docker 当前官方文档明确说明:在 containerd image store 下,data-root 不会自动改变 containerd image contents 和 snapshots 的位置。
于是可能变成:
1 | /mnt/docker-data Docker 其他数据 |
如果你的目标是彻底把大型镜像数据迁出根分区,就必须把 containerd 数据位置也纳入设计,而不是只修改 Docker data-root。
这类存储迁移涉及 daemon 停止、数据复制和配置一致性,不建议在磁盘快满时临时手工移动目录;应按 Docker/containerd 官方数据目录配置方式操作。
12. 一个 80 GiB 根分区为什么很容易不够
假设:
1 | 系统自身 15 GiB |
总量已经可能达到:
1 | 75~85 GiB |
如果这时还在根分区导出:
1 | cross-dev.tar.zst 10+ GiB |
很容易直接失败。
因此对超大型开发镜像,磁盘规划应该比“镜像 SIZE + 5 GiB”保守得多。
13. 更实用的容量规划方式
可以按几类数据分别估算:
1 | A. 操作系统和普通工具 |
其中 F 最适合放到另一块盘或网络存储,因为它只在迁移时需要。
如果这台机器长期维护大 SDK 镜像,更合理的硬件布局是:
1 | 系统根分区 |
14. 推荐的排查顺序
以后遇到 Docker 把磁盘吃满,可以按下面顺序:
1 | flowchart TD |
对应命令:
1 | df -h / |
这五步基本能把“大空间去哪了”分成明确类别。
15. 最后再决定处理策略
如果主要是 BuildKit cache:
1 | docker builder prune |
如果是明确不需要的旧镜像:
1 | docker image rm <image> |
如果是离线归档:
1 | 转移到移动硬盘 / NAS |
如果是 containerd image store 的最终镜像本身:
1 | 它不是“垃圾缓存” |
此时真正的解决方式可能是扩大数据分区、迁移 containerd 数据位置,或者重新评估是否必须长期保留如此巨大的完整 SDK 镜像。
关键原则只有一句:
先确认数据身份,再清理;不要用最激进的 prune 去替代诊断。
参考资料
- Docker Docs:containerd image store with Docker Engine — https://docs.docker.com/engine/storage/containerd/
- Docker Docs:Docker daemon data directory — https://docs.docker.com/engine/daemon/
- Docker Docs:Storage drivers — https://docs.docker.com/engine/storage/drivers/










