大型 Docker 开发镜像磁盘排查:containerd、缓存与容量规划
摘要:给出大型开发镜像的磁盘排查顺序,区分 containerd 镜像数据、snapshot、BuildKit cache、导出归档和源码构建输出。

文章目录
- 大型 Docker 开发镜像磁盘排查:containerd、缓存与容量规划
-
- 1. 第一层先看文件系统
- 2. 第二层看宿主机真实目录
- 3. 第三层看 Docker 逻辑对象
- 4. 构建过大镜像以后单独看 BuildKit cache
- 5. 为什么 builder prune 不会把 50 GiB 镜像变成 27 GiB
- 6. inactive 不等于“不需要”
- 7. image prune 和 system prune 的范围不同
- 8. 不要直接删除 `/var/lib/containerd`
- 9. 导出归档经常是被忽略的一大块
- 10. 源码 build 目录也可能很大
- 11. data-root 迁走后为什么根分区还可能增长
- 12. 一个 80 GiB 根分区为什么很容易不够
- 13. 更实用的容量规划方式
- 14. 推荐的排查顺序
- 15. 最后再决定处理策略
-
- 参考资料
当一个二十多 GiB 的开发镜像运行在 Docker Engine 29+ 新主机上时,磁盘问题往往比容器运行问题更早出现。
例如根分区只有约 80 GiB,却同时存在:
大型开发镜像
containerd content
unpacked snapshot
BuildKit cache
源码 build 输出
镜像导出归档
这时如果只看到:
/dev/sdaX 90%
然后立刻运行 docker system prune -a,很容易删掉重新构建代价很高的开发镜像。
更好的方式是先回答:
空间到底被哪一类数据占用了?
1. 第一层先看文件系统
df -h /
它回答的是:
整个根文件系统还剩多少空间?
它不会告诉你 Docker 哪个对象在占空间。
如果镜像归档放在其他分区,还要分别看:
df -h /path/to/archive
大型镜像导出时,Docker 数据和归档文件可能位于不同文件系统,不能只检查一个路径。
2. 第二层看宿主机真实目录
containerd image store 环境下,至少检查:
sudo du -sh /var/lib/containerd /var/lib/docker
这回答的是宿主机目录实际占用。
Docker 当前文档说明,containerd image store 下的 image contents 和 container snapshots 默认在:
/var/lib/containerd
所以新 Docker 环境中只看 /var/lib/docker 已经不够。
3. 第三层看 Docker 逻辑对象
docker system df
需要更详细时:
docker system df -v
它让你从 Docker 对象视角看:
Images
Containers
Local Volumes
Build Cache
du 和 docker system df 不要求数字完全一致。
一个是文件系统目录视角,一个是 Docker 对象和共享关系视角。
4. 构建过大镜像以后单独看 BuildKit cache
如果刚执行过大型 docker buildx build,再看:
docker buildx du
大型 SDK 构建经常产生很大的 BuildKit cache。
它和“最终镜像本身”不是同一个对象:
Build Cache
构建阶段复用
Final Image
docker run 使用
如果 cache 明确可回收,可以:
docker builder prune
这通常是大型镜像构建完成后的第一类清理对象。
5. 为什么 builder prune 不会把 50 GiB 镜像变成 27 GiB
如果当前 containerd image store 需要同时保存:
image content
unpacked snapshot
那么它们属于最终镜像运行体系。
执行:
docker builder prune
只会处理 BuildKit cache。
所以即使清掉 20 GiB 构建缓存,最终镜像相关的几十 GiB 仍然可能存在。
这不是 prune 失败,而是你清理的是不同对象。
6. inactive 不等于“不需要”
大型开发镜像有一个危险特点:平时不一定有常驻容器。
所以 Docker 可能显示:
ACTIVE 0
但这只说明现在没有容器使用它,不代表镜像应该删除。
一个 20+ GiB 开发镜像可能构建几个小时,甚至需要重新复制大型 SDK。
因此看到 inactive 后不要直接:
docker system prune -a
更合理的是先确定:
这个镜像是否是正式开发环境?
是否已经有可靠离线备份?
是否能快速重新构建?
7. image prune 和 system prune 的范围不同
较温和:
docker image prune
更激进:
docker image prune -a
范围更广:
docker system prune -a
越往后,自动判断“未使用”的对象范围越大。
对于普通 CI 临时环境,这可能很方便;对于手工制作的大型 SDK 镜像,应该更保守。
建议先:
docker system df -v
确认对象,再做针对性清理。
8. 不要直接删除 /var/lib/containerd
即使:
sudo du -sh /var/lib/containerd
显示几十 GiB,也不要执行:
sudo rm -rf /var/lib/containerd/*
containerd 的 content、metadata 和 snapshot 之间存在引用关系。
绕过 Docker/containerd 管理层手工删除,可能导致:
镜像 metadata 仍然存在
底层 blob 已损坏
snapshot 引用不一致
容器无法启动
底层目录不是普通“缓存目录”。
清理应优先通过 Docker CLI 或明确的官方迁移流程完成。
9. 导出归档经常是被忽略的一大块
例如你为了迁移镜像,在:
~/images/
放了:
cross-dev.tar
cross-dev.tar.zst
如果原始 TAR 没删,可能同时多占:
20+ GiB TAR
+
10+ GiB 压缩归档
它们完全不属于 Docker,因此:
docker system df
不会显示。
这就是为什么排查不能只看 Docker CLI。
可以检查:
du -sh ~/images
find ~/images -maxdepth 1 -type f -size +1G -ls
大型镜像场景里,“导出文件忘了删”非常常见。
10. 源码 build 目录也可能很大
如果开发容器使用 bind mount:
宿主机源码 -> /workspace
那么容器生成的:
build/
*.o
*.a
*.so
打包产物
都可能实际写在宿主机项目目录。
这些也不属于 Docker image store。
所以完整排查还应该看:
du -sh ~/work/demo-app
du -sh ~/work/demo-app/build
不要看到“是容器编译产生的”就认为一定在 /var/lib/docker。
11. data-root 迁走后为什么根分区还可能增长
Docker 常见配置:
{
"data-root": "/mnt/docker-data"
}
但 Docker 当前官方文档明确说明:在 containerd image store 下,data-root 不会自动改变 containerd image contents 和 snapshots 的位置。
于是可能变成:
/mnt/docker-data Docker 其他数据
/var/lib/containerd 镜像内容和 snapshot
如果你的目标是彻底把大型镜像数据迁出根分区,就必须把 containerd 数据位置也纳入设计,而不是只修改 Docker data-root。
这类存储迁移涉及 daemon 停止、数据复制和配置一致性,不建议在磁盘快满时临时手工移动目录;应按 Docker/containerd 官方数据目录配置方式操作。
12. 一个 80 GiB 根分区为什么很容易不够
假设:
系统自身 15 GiB
开发镜像相关数据 45~55 GiB
BuildKit cache 10 GiB
源码和 build 5 GiB
总量已经可能达到:
75~85 GiB
如果这时还在根分区导出:
cross-dev.tar.zst 10+ GiB
很容易直接失败。
因此对超大型开发镜像,磁盘规划应该比“镜像 SIZE + 5 GiB”保守得多。
13. 更实用的容量规划方式
可以按几类数据分别估算:
A. 操作系统和普通工具
B. containerd image content
C. unpacked snapshots
D. BuildKit cache 峰值
E. 源码和构建产物
F. 镜像导出归档
其中 F 最适合放到另一块盘或网络存储,因为它只在迁移时需要。
如果这台机器长期维护大 SDK 镜像,更合理的硬件布局是:
系统根分区
放 OS 和普通工具
大容量数据分区
放 Docker/containerd 数据
或至少放离线镜像归档
14. 推荐的排查顺序
以后遇到 Docker 把磁盘吃满,可以按下面顺序:
#mermaid-svg-MZlcQ853bmcUO81Q{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MZlcQ853bmcUO81Q .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MZlcQ853bmcUO81Q .error-icon{fill:#552222;}#mermaid-svg-MZlcQ853bmcUO81Q .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MZlcQ853bmcUO81Q .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MZlcQ853bmcUO81Q .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MZlcQ853bmcUO81Q .marker.cross{stroke:#333333;}#mermaid-svg-MZlcQ853bmcUO81Q svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MZlcQ853bmcUO81Q p{margin:0;}#mermaid-svg-MZlcQ853bmcUO81Q .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-MZlcQ853bmcUO81Q .cluster-label text{fill:#333;}#mermaid-svg-MZlcQ853bmcUO81Q .cluster-label span{color:#333;}#mermaid-svg-MZlcQ853bmcUO81Q .cluster-label span p{background-color:transparent;}#mermaid-svg-MZlcQ853bmcUO81Q .label text,#mermaid-svg-MZlcQ853bmcUO81Q span{fill:#333;color:#333;}#mermaid-svg-MZlcQ853bmcUO81Q .node rect,#mermaid-svg-MZlcQ853bmcUO81Q .node circle,#mermaid-svg-MZlcQ853bmcUO81Q .node ellipse,#mermaid-svg-MZlcQ853bmcUO81Q .node polygon,#mermaid-svg-MZlcQ853bmcUO81Q .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MZlcQ853bmcUO81Q .rough-node .label text,#mermaid-svg-MZlcQ853bmcUO81Q .node .label text,#mermaid-svg-MZlcQ853bmcUO81Q .image-shape .label,#mermaid-svg-MZlcQ853bmcUO81Q .icon-shape .label{text-anchor:middle;}#mermaid-svg-MZlcQ853bmcUO81Q .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MZlcQ853bmcUO81Q .rough-node .label,#mermaid-svg-MZlcQ853bmcUO81Q .node .label,#mermaid-svg-MZlcQ853bmcUO81Q .image-shape .label,#mermaid-svg-MZlcQ853bmcUO81Q .icon-shape .label{text-align:center;}#mermaid-svg-MZlcQ853bmcUO81Q .node.clickable{cursor:pointer;}#mermaid-svg-MZlcQ853bmcUO81Q .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MZlcQ853bmcUO81Q .arrowheadPath{fill:#333333;}#mermaid-svg-MZlcQ853bmcUO81Q .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MZlcQ853bmcUO81Q .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MZlcQ853bmcUO81Q .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MZlcQ853bmcUO81Q .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MZlcQ853bmcUO81Q .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MZlcQ853bmcUO81Q .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MZlcQ853bmcUO81Q .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MZlcQ853bmcUO81Q .cluster text{fill:#333;}#mermaid-svg-MZlcQ853bmcUO81Q .cluster span{color:#333;}#mermaid-svg-MZlcQ853bmcUO81Q div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MZlcQ853bmcUO81Q .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MZlcQ853bmcUO81Q rect.text{fill:none;stroke-width:0;}#mermaid-svg-MZlcQ853bmcUO81Q .icon-shape,#mermaid-svg-MZlcQ853bmcUO81Q .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MZlcQ853bmcUO81Q .icon-shape p,#mermaid-svg-MZlcQ853bmcUO81Q .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MZlcQ853bmcUO81Q .icon-shape .label rect,#mermaid-svg-MZlcQ853bmcUO81Q .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MZlcQ853bmcUO81Q .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MZlcQ853bmcUO81Q .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MZlcQ853bmcUO81Q :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
df -h:文件系统还剩多少
du:/var/lib/containerd 与 /var/lib/docker
docker system df -v:镜像/容器/volume
docker buildx du:BuildKit cache
检查导出归档和源码 build
针对确认对象清理或迁移
对应命令:
df -h /
sudo du -sh /var/lib/containerd /var/lib/docker
docker system df -v
docker buildx du
du -sh ~/images ~/work/demo-app/build 2>/dev/null
这五步基本能把“大空间去哪了”分成明确类别。
15. 最后再决定处理策略
如果主要是 BuildKit cache:
docker builder prune
如果是明确不需要的旧镜像:
docker image rm <image>
如果是离线归档:
转移到移动硬盘 / NAS
或确认备份后删除本地副本
如果是 containerd image store 的最终镜像本身:
它不是“垃圾缓存”
此时真正的解决方式可能是扩大数据分区、迁移 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/



