一、问题背景
在需要将 Docker 镜像从联网环境迁移到离线环境时,一般会使用:
1 | docker pull <image> |
然后将 image.tar 拷贝到离线环境:
1 | docker load -i image.tar |
本次在迁移:
1 | vllm/vllm-openai:latest |
镜像时,却遇到了一个非常异常的问题:
- 本地镜像大小约 30GB;
docker save导出的 tar 文件只有约 17KB;- tar 文件可以正常解包,并且是 OCI Image Layout;
- 归档中只有少量 manifest / metadata;
- 真正的 filesystem layer 并没有被导出;
- 将 tar 文件带到离线环境后无法正常使用;
- 指定
--platform=linux/amd64导出时,Docker 又提示该镜像不提供 amd64 平台。
最终确认,该现象与 Docker 29.2 + containerd image store 下的一个 Moby 官方 Bug 高度一致。
最终通过一次“空 Build”成功解决,而且不需要重新下载约 30GB 的 vLLM 镜像。
二、系统环境
本次出现问题的实际环境如下。
Docker Client
1 | Version: 29.2.0 |
Docker Server
1 | Docker Desktop: 4.59.0 |
containerd
1 | Version: v2.2.1 |
runc
1 | Version: 1.3.4 |
目标镜像:
1 | vllm/vllm-openai:latest |
本地镜像大小约:
1 | 30GB |
三、异常现象
正常情况下,一个约 30GB 的 Docker 镜像执行:
1 | docker save \ |
得到的 tar 至少应该是 GB 级。
但实际生成的文件只有:
1 | 约 17KB |
这显然不可能是正常压缩。
进一步查看归档内容:
1 | tar -tf openaivllmlatest.tar |
得到:
1 | blobs/ |
这里已经能够发现明显异常。
一个约 30GB 的大型 CUDA/vLLM 镜像,正常情况下:
1 | blobs/sha256/ |
下面应该存在大量:
1 | config |
而这里实际只有两个 blob。
也就是说:
30GB 并不是被压缩成了 17KB,而是几十 GB 的镜像 Layer 根本没有进入
docker save生成的 tar。
四、进一步分析 Manifest
继续读取其中一个 blob:
1 | tar -xOf openaivllmlatest.tar \ |
得到:
1 | { |
Manifest List 中明确存在:
1 | linux/arm64 |
于是继续尝试:
1 | docker image save \ |
Docker 却返回:
1 | Error response from daemon: |
这时出现了一个非常有意思的矛盾:
1 | Manifest List |
因此问题并不是:
1 | vllm/vllm-openai 不支持 amd64 |
而是:
Docker 本地 image store 中虽然存在对应的 Manifest 信息,但对应平台的 Layer/Blob 没有处于
docker save能够正确导出的状态。
五、官方 Issue
这个问题与 Moby 官方 Issue #52193 高度一致。
Issue 标题:
1 | c8d storage issue with `docker save` after `docker pull` |
官方地址:
https://github.com/moby/moby/issues/52193
该 Issue 创建于:
1 | 2026-03-19 |
并带有:
1 | containerd-integration |
等标签。
官方 Issue 的复现也是先执行:
1 | docker pull --platform ... |
然后执行:
1 | docker save --platform ... |
最终得到:
1 | no suitable export target found: |
Issue 中出现问题的环境包括:
1 | Docker Engine 29.2.1 |
而我的环境是:
1 | Docker Engine 29.2.0 |
两者高度接近。
更加关键的是,该 Issue 还明确说明:
1 | Blobs that are expected to be present are missing |
也就是说,本应存在的 Blob 实际无法被 exporter 获取,从而可能进一步产生:
1 | missing layers |
等异常。
六、真正有效的解决方案
Moby #52193 中同时给出了一个非常有效的 workaround:
对准备进行
docker save的镜像执行一次 Build。
本次实际测试同样有效。
执行:
1 | docker build \ |
这里实际上相当于创建了一个只有一行内容的 Dockerfile:
1 | FROM vllm/vllm-openai:latest |
没有:
1 | RUN |
也没有修改原始 vLLM 镜像里的任何内容。
它只是重新基于原镜像生成一个本地 Build 结果:
1 | vllm/vllm-openai:latest |
完成以后重新导出:
1 | docker image save \ |
这一次:
1 | docker save |
恢复正常。
生成的 tar 文件重新变成 GB 级,并且包含完整镜像 Layer。
之后即可复制到离线环境:
1 | docker load -i openaivllm-amd64.tar |
镜像可以正常使用。
七、为什么一次“空 Build”能够解决问题?
这部分需要注意,目前更适合作为根据官方 Issue 和实际测试结果进行的机制分析,而不是把它描述成 Docker 官方已经完全确认的底层根因。
问题发生时,可以简单理解成:
1 | docker pull |
而执行:
1 | FROM vllm/vllm-openai:latest |
进行一次 Build 后,Docker/BuildKit 需要重新解析:
1 | 镜像 Manifest |
并生成新的本地构建结果。
可以将这个过程理解成:
1 | 原镜像 |
因此,与其说:
1 | 镜像损坏了 |
不如说:
Docker 29.2 + containerd image store 场景下,
docker pull后的镜像在docker save导出过程中出现了 Blob/Layer 的可见性或导出状态异常;重新执行 Build 可以触发 Docker/BuildKit 重新处理这些 Layer,从而使其恢复为可正常导出的状态。
八、如何确认自己遇到的是同一个问题?
如果出现以下组合现象,就非常值得怀疑是相同问题。
首先:
1 | docker images |
镜像显示:
1 | 10GB |
甚至更大。
但是:
1 | docker save -o image.tar image:tag |
得到的文件却只有:
1 | 几 KB |
查看:
1 | tar -tf image.tar |
发现只有:
1 | blobs/ |
并且:
1 | blobs/sha256/ |
下面只有极少量文件。
同时再执行:
1 | docker save \ |
出现:
1 | no suitable export target found |
或者:
1 | image ... was found but does not provide the specified platform |
而 Manifest List 中明明存在:
1 | linux/amd64 |
那么这个问题与 Moby #52193 就非常接近了。
九、导出以后一定要进行验证
问题解决以后,不建议看到 docker save 没有报错就直接把文件带进离线环境。
至少做下面几个检查。
1. 检查文件大小
1 | du -h openaivllm-amd64.tar |
如果原镜像是几十 GB:
1 | 17KB |
显然不正常。
恢复以后应该是:
1 | GB 级 |
2. 检查 Blob 数量
1 | tar -tf openaivllm-amd64.tar \ |
原来的异常镜像只有极少量 Blob。
正常的大型 vLLM 镜像应该存在大量:
1 | blobs/sha256/xxxxxxxx |
3. 查看归档内容
1 | tar -tf openaivllm-amd64.tar | head -50 |
应该能够看到大量:
1 | blobs/sha256/xxxx |
4. 计算 SHA256
在线机器:
1 | sha256sum openaivllm-amd64.tar |
复制到离线环境以后:
1 | sha256sum openaivllm-amd64.tar |
两边结果应该完全一致。
这样可以排除移动硬盘、U盘、网络传输等过程导致的大文件损坏。
十、备用方案:关闭 containerd image store
Moby #52193 中还提到:
关闭 Docker Desktop 中的:
1 | Use containerd for pulling and storing images |
以后,docker save 可以恢复正常。
Docker Desktop 可以进入:
1 | Settings |
取消该选项。
Docker 官方关于 containerd image store 的说明:
https://docs.docker.com/desktop/features/containerd/
需要注意的是:
classic image store 与 containerd image store 是两套独立的镜像存储。
因此切换以后,之前的镜像可能暂时不会出现在:
1 | docker images |
中。
这并不代表镜像被删除。
重新切回原来的 image store 后又可以看到。
如果镜像有:
1 | 20GB |
那么切换 image store 后重新 docker pull 的成本比较高。
所以对于这种大型 AI/CUDA 镜像,我更推荐优先尝试:
1 | 空 Build |
成功以后直接:
1 | docker save |
十一、推荐排查流程
以后遇到类似问题,可以按照:
1 | docker save 文件异常小 |
十二、最终可直接使用的命令
如果以后再次遇到类似情况,可以直接尝试:
1. 重新生成本地镜像
1 | docker build \ |
2. 导出 amd64 镜像
1 | docker image save \ |
3. 检查文件大小
1 | du -h openaivllm-amd64.tar |
4. 检查 Blob
1 | tar -tf openaivllm-amd64.tar \ |
5. 生成 SHA256
1 | sha256sum openaivllm-amd64.tar |
6. 离线机器加载
1 | docker load -i openaivllm-amd64.tar |
十三、总结
这次问题最具有迷惑性的地方在于:
1 | docker images |
明明显示:
1 | 约 30GB |
但:
1 | docker save |
却只生成:
1 | 约 17KB |
进一步检查 OCI archive 后发现,17KB 文件基本只有:
1 | OCI Index |
真正几十 GB 的 filesystem layer 并没有进入 tar。
同时:
1 | docker save --platform=linux/amd64 |
还出现:
1 | image ... was found but does not provide the specified platform |
但是 Manifest List 明明声明存在:
1 | linux/amd64 |
结合:
1 | Docker Engine 29.2.0 |
以及 Moby 官方 Issue #52193,可以判断该问题与 Docker 29.2 下 containerd image store 的 docker save Bug 高度一致。
最终解决方法非常简单:
1 | docker build \ |
然后重新:
1 | docker image save \ |
即可恢复正常。
对于几十 GB 的 vLLM、PyTorch、CUDA 等大型 AI 镜像,这种方案最大的优点是:
通常不需要重新下载整个镜像,就能解决
docker save缺少 Layer 的问题。
十四、参考资料
Moby 官方 Issue
#52193 - c8d storage issue with docker save after docker pull
https://github.com/moby/moby/issues/52193
Moby 相关历史 Issue
#47065 - [v25] docker save produces an OCI manifest with no layers
https://github.com/moby/moby/issues/47065
Docker 官方文档:containerd image store
https://docs.docker.com/desktop/features/containerd/
Docker 官方文档:docker image save
https://docs.docker.com/reference/cli/docker/image/save/