Docker 29 与 containerd image store:镜像为什么会保存两种形态
摘要:从 content store 与 snapshotter 两个角色解释 Docker Engine 29 的 containerd image store,以及大型镜像为什么比传统 overlay2 更占磁盘。
@[toc]
把一个大型开发镜像迁移到新的 Ubuntu 主机后,有时会看到一个很反直觉的结果:
1 | 镜像内容大约 27 GiB |
如果新机器使用 Docker Engine 29+ 的全新安装,这个现象很可能与 containerd image store 有关。
Docker 官方当前文档明确说明:Docker Engine 29.0 及以后版本的全新安装,默认使用 containerd image store;从较早版本升级的系统则可能继续使用传统 graph driver。
1. 先确认当前后端
Docker 官方提供的检查命令是:
1 | docker info -f '{{ .DriverStatus }}' |
如果输出类似:
1 | [[driver-type io.containerd.snapshotter.v1]] |
说明当前正在使用 containerd image store。
Docker Engine 默认使用 containerd 的 overlayfs snapshotter 管理解包后的镜像文件系统。
这意味着两台同样是 Ubuntu 24.04 的机器,因为 Docker 安装历史不同,也可能表现不一样:
1 | 旧安装升级:可能继续 overlay2 |
2. 为什么同一张镜像要保存两种形态
最直观的理解是:
1 | 镜像既要“运输”,又要“运行” |
这两种需求的数据形态不同。
可以类比为:
1 | 运输形态 运行形态 |
Docker 官方对 containerd image store 的磁盘说明非常明确:它会保留压缩形式的 image layers,同时还会把它们解包到磁盘,因此相同镜像通常比传统 storage driver 占更多空间。
3. content store 是“镜像内容”
从 OCI/Registry 角度,一张镜像并不是一个普通 Linux 目录,而是一组内容对象:
1 | Image |
它们通常使用 digest 标识:
1 | sha256:... |
这类内容适合:
1 | docker pull |
containerd 的 content store 管理的就是这类可分发内容。
保留这些内容的一个直接好处是:push 时不需要每次重新扫描完整根文件系统,再重新制造镜像层。
4. snapshotter 是“可运行文件系统”
但容器真正运行时需要看到:
1 | / |
因此 layer 还需要 unpack。
1 | flowchart LR |
snapshotter 管理的是已经准备好用于容器挂载的文件系统状态。
这样执行:
1 | docker run cross-dev:20.04 |
时,就不需要每次重新解压二十多 GiB 内容。
5. 所以“接近两倍”不等于重复导入
假设镜像内容约:
1 | 27 GiB |
而宿主机相关数据约:
1 | 50+ GiB |
可以先用下面这个模型理解:
1 | Docker Image |
但不要机械得出:
1 | 27 × 2 = 必然 54 GiB |
真实数字还会受到:
1 | 压缩率 |
影响。
正确说法应该是:
containerd image store 同时保存分发形态和运行形态,因此大型镜像会明显放大磁盘占用,但不是固定的 2 倍公式。
6. 为什么普通镜像看起来没问题
对于 500 MB 或 1 GiB 镜像,多占几百 MB 往往不明显。
但交叉编译开发镜像可能包含:
1 | 完整 Ubuntu 用户空间 |
当单镜像已经达到二三十 GiB 时,存储模型中的额外一份形态会直接放大成几十 GiB。
所以问题并不是 containerd 对普通镜像“突然失控”,而是大型 SDK 把差异放大了。
7. containerd image store 为什么成为默认
Docker Engine 29 选择 containerd image store,并不仅仅是为了替换 overlay2 目录结构。
Docker 官方列出的能力包括:
1 | 本地保存多平台镜像 |
传统 classic image store 对这些现代 OCI 场景支持有限。
所以 containerd image store 的设计目标不只是:
1 | “本机能不能运行这一个容器” |
而是同时覆盖镜像分发、身份、现代 manifest/index 和运行时文件系统。
8. /var/lib/containerd 为什么开始重要
传统 Docker 环境中,很多人习惯只看:
1 | sudo du -sh /var/lib/docker |
但使用 containerd image store 时,Docker 当前文档说明 image contents 和 container snapshots 默认位于:
1 | /var/lib/containerd |
而其他 Docker daemon 数据仍可能位于:
1 | /var/lib/docker |
因此新环境应该同时看:
1 | sudo du -sh /var/lib/docker /var/lib/containerd |
这也是为什么把旧经验直接套到 Docker 29 新安装上,会得到错误判断。
9. Docker data-root 也出现了新的边界
过去常见做法是在 /etc/docker/daemon.json 中配置:
1 | { |
把 Docker 数据迁到大磁盘。
但 Docker 当前官方文档特别提醒:使用 containerd image store 时,Docker data-root 不会自动改变 containerd image/snapshot 的存储位置。
于是可能出现:
1 | /var/lib/docker -> 已迁到大盘 |
然后根分区还是不断变满。
对于大型开发镜像,这是一个必须提前了解的变化。
10. 切换 storage backend 不是清理操作
有人看到 containerd image store 更占空间,会想到切回 overlay2。
需要注意 Docker 官方说明:切换 storage backend 会让另一套 backend 中已经存在的镜像和容器暂时不可见,但数据仍然留在磁盘。
也就是说:
1 | 镜像“看不见” |
如果只是为了腾磁盘而来回切换后端,反而可能让系统中同时保留两套数据,更难排查。
因此后端切换应该被当作存储架构选择,而不是 prune 的替代品。
11. 一张图收敛这个模型
1 | flowchart TB |
真正需要记住的是:
1 | content store:镜像作为可分发内容的形态 |
containerd image store 会让这两类数据同时存在。
所以一个 20+ GiB 的开发镜像迁移到 Docker Engine 29+ 新主机后,看到 /var/lib/containerd 迅速增长并不一定是异常。
下一步应该先区分 image content、snapshot 和 BuildKit cache,再决定哪些空间可以安全释放。
参考资料
- Docker Docs:containerd image store with Docker Engine — https://docs.docker.com/engine/storage/containerd/
- Docker Docs:Storage drivers — https://docs.docker.com/engine/storage/drivers/
- Docker Engine 29 release notes — https://docs.docker.com/engine/release-notes/29/










