april.a digital gardenAPRILNOTE!
↖ 全部笔记
April

Docker pull镜像save后仅导出几KB:镜像 Layer 缺失问题排查与解决

# Docker

一、问题背景

在需要将 Docker 镜像从联网环境迁移到离线环境时,一般会使用:

1
2
3
docker pull <image>

docker save -o image.tar <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
2
3
4
5
6
Version:           29.2.0
API version: 1.53
Go version: go1.25.6
Git commit: 0b9d198
OS/Arch: linux/amd64
Context: default

Docker Server

1
2
3
4
5
Docker Desktop:    4.59.0
Engine Version: 29.2.0
API version: 1.53
OS/Arch: linux/amd64
Experimental: false

containerd

1
2
Version:           v2.2.1
GitCommit: dea7da592f5d1d2b7755e3a161be07f43fad8f75

runc

1
Version:           1.3.4

目标镜像:

1
vllm/vllm-openai:latest

本地镜像大小约:

1
30GB

三、异常现象

正常情况下,一个约 30GB 的 Docker 镜像执行:

1
2
3
docker save \
-o openaivllmlatest.tar \
vllm/vllm-openai:latest

得到的 tar 至少应该是 GB 级。

但实际生成的文件只有:

1
约 17KB

这显然不可能是正常压缩。

进一步查看归档内容:

1
tar -tf openaivllmlatest.tar

得到:

1
2
3
4
5
6
7
blobs/
blobs/sha256/
blobs/sha256/07ea4e292adf3a26b05ac97114b28849cf4551a26beb1fbe7decd3842d752ed7
blobs/sha256/5b03c7de2951cda2d5fd087457902625eabbe9188478e439561ef34f325b0fd5
index.json
manifest.json
oci-layout

这里已经能够发现明显异常。

一个约 30GB 的大型 CUDA/vLLM 镜像,正常情况下:

1
blobs/sha256/

下面应该存在大量:

1
2
3
config
manifest
filesystem layer

而这里实际只有两个 blob。

也就是说:

30GB 并不是被压缩成了 17KB,而是几十 GB 的镜像 Layer 根本没有进入 docker save 生成的 tar。


四、进一步分析 Manifest

继续读取其中一个 blob:

1
2
3
tar -xOf openaivllmlatest.tar \
blobs/sha256/07ea4e292adf3a26b05ac97114b28849cf4551a26beb1fbe7decd3842d752ed7 \
| jq .

得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7238,
"digest": "sha256:7441a0579b7974fe00eec2c41a5710bcd0dc3dafc03edcdc5663158dd68286cf",
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 7239,
"digest": "sha256:5b03c7de2951cda2d5fd087457902625eabbe9188478e439561ef34f325b0fd5",
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
]
}

Manifest List 中明确存在:

1
2
linux/arm64
linux/amd64

于是继续尝试:

1
2
3
4
docker image save \
--platform=linux/amd64 \
-o openaivllm-amd64.tar \
vllm/vllm-openai:latest

Docker 却返回:

1
2
3
4
Error response from daemon:
no suitable export target found:
image with reference vllm/vllm-openai:latest was found
but does not provide the specified platform (linux/amd64)

这时出现了一个非常有意思的矛盾:

1
2
3
4
5
6
7
8
9
10
11
Manifest List
│
├── linux/arm64
│
└── linux/amd64
│
▼
docker save --platform=linux/amd64
│
▼
Docker:没有 linux/amd64

因此问题并不是:

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
2
3
containerd-integration
kind/bug
version/29.2

等标签。

官方 Issue 的复现也是先执行:

1
docker pull --platform ...

然后执行:

1
docker save --platform ...

最终得到:

1
2
no suitable export target found:
image ... was found but does not provide the specified platform

Issue 中出现问题的环境包括:

1
2
3
Docker Engine 29.2.1
containerd 2.2.1
Docker Desktop

而我的环境是:

1
2
3
Docker Engine 29.2.0
containerd 2.2.1
Docker Desktop 4.59.0

两者高度接近。

更加关键的是,该 Issue 还明确说明:

1
Blobs that are expected to be present are missing

也就是说,本应存在的 Blob 实际无法被 exporter 获取,从而可能进一步产生:

1
2
missing layers
no such file or directory

等异常。


六、真正有效的解决方案

Moby #52193 中同时给出了一个非常有效的 workaround:

对准备进行 docker save 的镜像执行一次 Build。

本次实际测试同样有效。

执行:

1
2
3
4
5
6
docker build \
--platform=linux/amd64 \
-t vllm/vllm-openai:offline \
- <<'EOF'
FROM vllm/vllm-openai:latest
EOF

这里实际上相当于创建了一个只有一行内容的 Dockerfile:

1
FROM vllm/vllm-openai:latest

没有:

1
2
3
RUN
COPY
ADD

也没有修改原始 vLLM 镜像里的任何内容。

它只是重新基于原镜像生成一个本地 Build 结果:

1
2
3
4
5
vllm/vllm-openai:latest
│
│ FROM
▼
vllm/vllm-openai:offline

完成以后重新导出:

1
2
3
4
docker image save \
--platform=linux/amd64 \
-o openaivllm-amd64.tar \
vllm/vllm-openai:offline

这一次:

1
docker save

恢复正常。

生成的 tar 文件重新变成 GB 级,并且包含完整镜像 Layer。

之后即可复制到离线环境:

1
docker load -i openaivllm-amd64.tar

镜像可以正常使用。


七、为什么一次“空 Build”能够解决问题?

这部分需要注意,目前更适合作为根据官方 Issue 和实际测试结果进行的机制分析,而不是把它描述成 Docker 官方已经完全确认的底层根因。

问题发生时,可以简单理解成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker pull
│
▼
containerd image store
│
├── OCI Index 存在
├── Manifest 存在
└── Blob / Layer 导出状态异常
│
▼
docker save
│
▼
只能导出少量 Metadata
│
▼
17KB

而执行:

1
FROM vllm/vllm-openai:latest

进行一次 Build 后,Docker/BuildKit 需要重新解析:

1
2
3
4
镜像 Manifest
平台
Config
基础镜像 Layer

并生成新的本地构建结果。

可以将这个过程理解成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
原镜像
│
│ docker pull
▼
containerd image store
│
└── Layer/Blob 对 save 不可正常导出
│
▼
docker build
│
▼
BuildKit 重新解析基础镜像
│
▼
生成新的本地 Image Result
│
▼
Layer/Blob 可以被 exporter 获取
│
▼
docker save 正常

因此,与其说:

1
镜像损坏了

不如说:

Docker 29.2 + containerd image store 场景下,docker pull 后的镜像在 docker save 导出过程中出现了 Blob/Layer 的可见性或导出状态异常;重新执行 Build 可以触发 Docker/BuildKit 重新处理这些 Layer,从而使其恢复为可正常导出的状态。


八、如何确认自己遇到的是同一个问题?

如果出现以下组合现象,就非常值得怀疑是相同问题。

首先:

1
docker images

镜像显示:

1
2
3
10GB
20GB
30GB

甚至更大。

但是:

1
docker save -o image.tar image:tag

得到的文件却只有:

1
2
几 KB
几十 KB

查看:

1
tar -tf image.tar

发现只有:

1
2
3
4
5
blobs/
blobs/sha256/
index.json
manifest.json
oci-layout

并且:

1
blobs/sha256/

下面只有极少量文件。

同时再执行:

1
2
3
4
docker save \
--platform=linux/amd64 \
-o image.tar \
image:tag

出现:

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
2
3
tar -tf openaivllm-amd64.tar \
| grep '^blobs/sha256/' \
| wc -l

原来的异常镜像只有极少量 Blob。

正常的大型 vLLM 镜像应该存在大量:

1
blobs/sha256/xxxxxxxx

3. 查看归档内容

1
tar -tf openaivllm-amd64.tar | head -50

应该能够看到大量:

1
2
3
4
blobs/sha256/xxxx
blobs/sha256/xxxx
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
2
3
Settings
→ General
→ Use containerd for pulling and storing images

取消该选项。

Docker 官方关于 containerd image store 的说明:

https://docs.docker.com/desktop/features/containerd/

需要注意的是:

classic image store 与 containerd image store 是两套独立的镜像存储。

因此切换以后,之前的镜像可能暂时不会出现在:

1
docker images

中。

这并不代表镜像被删除。

重新切回原来的 image store 后又可以看到。

如果镜像有:

1
2
3
20GB
30GB
40GB

那么切换 image store 后重新 docker pull 的成本比较高。

所以对于这种大型 AI/CUDA 镜像,我更推荐优先尝试:

1
空 Build

成功以后直接:

1
docker save

十一、推荐排查流程

以后遇到类似问题,可以按照:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
docker save 文件异常小
│
▼
tar -tf 检查内容
│
▼
是否只有 Manifest / Metadata?
│
Yes
│
▼
检查 docker version
检查 containerd version
│
▼
docker save --platform
│
▼
是否提示 platform 不存在?
│
Yes
│
▼
执行一次空 Build
│
▼
docker save 新镜像
│
┌───┴────┐
│ │
成功 失败
│ │
▼ ▼
完成 关闭 containerd
image store
│
▼
重新 pull
│
▼
docker save

十二、最终可直接使用的命令

如果以后再次遇到类似情况,可以直接尝试:

1. 重新生成本地镜像

1
2
3
4
5
6
docker build \
--platform=linux/amd64 \
-t vllm/vllm-openai:offline \
- <<'EOF'
FROM vllm/vllm-openai:latest
EOF

2. 导出 amd64 镜像

1
2
3
4
docker image save \
--platform=linux/amd64 \
-o openaivllm-amd64.tar \
vllm/vllm-openai:offline

3. 检查文件大小

1
du -h openaivllm-amd64.tar

4. 检查 Blob

1
2
3
tar -tf openaivllm-amd64.tar \
| grep '^blobs/sha256/' \
| wc -l

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
2
3
OCI Index
Manifest
Metadata

真正几十 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
2
3
Docker Engine 29.2.0
Docker Desktop 4.59.0
containerd 2.2.1

以及 Moby 官方 Issue #52193,可以判断该问题与 Docker 29.2 下 containerd image store 的 docker save Bug 高度一致。

最终解决方法非常简单:

1
2
3
4
5
6
docker build \
--platform=linux/amd64 \
-t vllm/vllm-openai:offline \
- <<'EOF'
FROM vllm/vllm-openai:latest
EOF

然后重新:

1
2
3
4
docker image save \
--platform=linux/amd64 \
-o openaivllm-amd64.tar \
vllm/vllm-openai:offline

即可恢复正常。

对于几十 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/

Docker 官方文档:docker image load

https://docs.docker.com/reference/cli/docker/image/load/

查找笔记