前置阅读:建议先阅读 镜像签名与供应链验证:Cosign/Sigstore 与准入控制深度实践 与 镜像安全扫描与 SBOM:Trivy、Grype、Clair 与 CI 安全门禁。本篇聚焦 OCI 规范本身的 manifest/index/artifact 结构与工件生态。
我们每天 docker pull 的“镜像”,本质上不是一个大 tar 包,而是一套由 OCI(Open Container Initiative) 标准化的、内容寻址的分发格式:一个 manifest 指向若干 blob,每个 blob 用 sha256 摘要钉死。理解这套结构,是从“会用 Docker”走向“能排查 registry、能设计供应链”的分水岭——当你看到 manifest unknown、no matching manifest for linux/arm64、referrers not supported 这类报错时,答案全在这套规范里。
1. OCI Image Spec 三大组件与 media type
OCI Image Spec(v1.1.0)把一个镜像拆成三类 blob,全部以内容摘要寻址:
- manifest:JSON 文档,声明这个镜像由哪些 blob 组成,以及每个 blob 的 media type、size、digest。
- config:JSON 文档,记录架构、OS、环境变量、入口命令,以及
rootfs.diff_ids。 - layers:实际的 tar 层(通常 tar+gzip 或 tar+zstd),每层一个独立 blob。
三者不是“包含”关系,而是“引用”关系:manifest 里存的是 config 与 layers 的摘要,真正的字节流各自躺在 registry 的 blob 存储里。
1.1 内容寻址:一切以 digest 为锚
registry 是 content-addressable 存储:blob 的访问路径就是它的摘要,tag 只是一个可变的“别名指针”。
tag → manifest digest → config digest + layer digests → blobs
v1.2 → sha256:aaa... → sha256:bbb... / sha256:ccc... → ...
这解释了为什么“同一 tag 在不同时间拉到的内容可能不同”(tag 被重新推送),而“同一 digest 永远是同一内容”。生产环境用 digest 引用是不可变性的基石。
1.2 常见 media type 一览
| 用途 | OCI media type | Docker 等价 |
|---|---|---|
| 单平台 manifest | application/vnd.oci.image.manifest.v1+json | application/vnd.docker.distribution.manifest.v2+json |
| 多平台 index | application/vnd.oci.image.index.v1+json | application/vnd.docker.distribution.manifest.list.v2+json |
| 镜像 config | application/vnd.oci.image.config.v1+json | application/vnd.docker.container.image.v1+json |
| tar+gzip 层 | application/vnd.oci.image.layer.v1.tar+gzip | application/vnd.docker.image.rootfs.diff.tar.gzip |
| tar+zstd 层 | application/vnd.oci.image.layer.v1.tar+zstd | 无(Docker 不支持 zstd 层) |
一句话:media type 就是 blob 的“身份证类型”,客户端靠它决定怎么解析——类型写错,registry 未必拦你,但运行时一定炸。
2. manifest 详解与 Docker schema2 差异
2.1 一个完整的 OCI manifest
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"artifactType": "application/vnd.example.sbom.v1+json",
"config": {
"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7",
"size": 7023
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736",
"size": 32654,
"annotations": { "org.opencontainers.image.title": "layer.tar.gz" }
}
],
"annotations": { "org.opencontainers.image.source": "https://github.com/example/app" }
}
注意 config 和每个 layers 元素都是同一种结构——descriptor:mediaType + digest + size(可选 annotations、urls、platform)。整个 OCI 规范就是“用 descriptor 层层引用”的递归结构。
2.2 与 Docker schema2 的字段级差异
| 能力 | OCI manifest v1 | Docker schema2 |
|---|---|---|
| 顶层 annotations | 支持 | 不支持 |
| artifactType | 支持(1.1 引入) | 无此字段 |
| subject(关联上游工件) | 支持(1.1 引入) | 无此字段 |
| 层压缩算法 | gzip / zstd / 不压缩 | 仅 gzip |
| 空 config 表达纯工件 | 支持 oci.empty.v1+json | 必须是一个真实 image config |
artifactType 与 subject 是 OCI 1.1 的关键增量:前者声明“这个工件是什么”(如 SBOM、签名、Helm chart),后者声明“这个工件指向谁”(如“我是镜像 sha256:aaa 的 SBOM”)。Docker schema2 时代只能用“约定俗成的 tag”(如 sha256-aaa.sbom)来模拟关联,非常脆弱。
digest 是对 blob 的原始字节做 sha256,不是对“解析后的 JSON 对象”算。所以 JSON 里的空格、换行、键顺序都会改变 digest;你不能“格式化”一个 manifest 后还指望 digest 不变。校验方式:把 blob 原样读出 → sha256 → 与 descriptor 里的 digest 逐字符比对。
一句话:digest 是内容的指纹,任何一字节差异都会让它面目全非——这正是不可变性和防篡改的根基。
3. image index 与多架构
3.1 index 的结构
多架构镜像不是“一个镜像”,而是一个 index(Docker 叫 manifest list),它列出每个平台对应的 manifest descriptor:
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:1a2b3c...",
"size": 1234,
"platform": { "architecture": "amd64", "os": "linux" }
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:4d5e6f...",
"size": 1234,
"platform": { "architecture": "arm64", "os": "linux", "variant": "v8" }
}
]
}
3.2 index 与 manifest list 的差异
| 维度 | OCI image index | Docker manifest list |
|---|---|---|
| media type | vnd.oci.image.index.v1+json | vnd.docker.distribution.manifest.list.v2+json |
| 成员类型 | 可混入 index 与 manifest | 只能是 schema2 manifest |
| annotations | 支持 | 不支持 |
| 能否含非镜像成员 | 可以(配合 artifactType) | 不可以 |
拉取时客户端按本机 os / architecture / variant 在 index 里匹配 descriptor;匹配不到就报 no matching manifest for linux/arm64 in the manifest list entries。可以显式覆盖:
docker pull --platform linux/amd64 nginx:1.27
docker buildx build --platform linux/amd64,linux/arm64 -t repo/app:1.0 --push .
--platform 指定的平台若不在 index 中,Docker 会尝试用 docker run --platform 走 QEMU 模拟——但模拟只影响运行,不代表 index 里有对应 manifest。
4. config 与 rootfs.diff_ids
4.1 config 结构
{
"architecture": "amd64",
"os": "linux",
"config": {
"Env": ["PATH=/usr/local/sbin:/usr/local/bin"],
"Entrypoint": ["/entrypoint.sh"],
"Cmd": ["serve"]
},
"rootfs": {
"type": "layers",
"diff_ids": ["sha256:6f9a1e0a...", "sha256:8b7c6d5e..."]
}
}
4.2 两种 digest 的区别与常见混淆点
| 位置 | 摘要含义 | 对什么算 |
|---|---|---|
| manifest.layers[].digest | 压缩后 blob 的摘要 | registry 里存的 gzip/zstd 字节 |
| config.rootfs.diff_ids[] | 解压后 tar 的摘要 | 未压缩的 tar 流 |
两者一一对应、顺序相同。校验链条是:拉 layer blob → 先对压缩字节算 sha256 比对 manifest → 再解压 → 对解压流算 sha256 比对 diff_ids。任何一层被篡改,两条链都会断。
一句话:
diff_ids是“解压后的指纹”,layers[].digest是“压缩后的指纹”,它们共同保证层内容在传输与解压两个环节都没被动过。
5. 查看与操作 manifest 的工具
5.1 docker manifest inspect
docker manifest inspect nginx:1.27
docker manifest inspect -v nginx:1.27 | grep architecture
5.2 docker buildx imagetools inspect
docker buildx imagetools inspect nginx:1.27
# 拿到某平台 manifest 的原始 JSON 与 digest
docker buildx imagetools inspect nginx:1.27 --raw
docker buildx imagetools inspect nginx:1.27 --format '{{json .Manifest}}'
5.3 skopeo 与 crane 的免本地存储搬运
# skopeo 直接读远端原始 manifest,不落盘
skopeo inspect --raw docker://docker.io/library/nginx:1.27
# crane 是 go-containerregistry 的 CLI,适合脚本化
crane manifest nginx:1.27
crane digest nginx:1.27
skopeo inspect --raw 与 crane manifest 拿到的是未经解析的原始 JSON,是排查“digest 对不上”“media type 异常”的第一手证据;docker manifest inspect 默认会做一层美化与平台聚合,反而可能掩盖字段细节。
6. OCI Artifacts 规范:artifactType 与 subject
OCI 1.1 把 registry 从“只存镜像”升级为“可存任意工件”:任何能表达成 manifest 的东西(SBOM、签名、Helm chart、WASM 模块、策略包)都能推上去。关键就两个字段:
- artifactType:声明工件类型,如
application/vnd.cyclonedx+json、application/vnd.cncf.helm.config.v1+json。 - subject:声明本工件“附属于”哪个镜像 digest。
6.2 referrers API
要查询“镜像 X 有哪些附属工件”,客户端调用 referrers API:
GET /v2/<name>/referrers/<digest>
→ 返回一个 index,列出所有 subject 指向该 digest 的工件
# oras 查询某镜像的所有 referrers
oras discover --format json registry.example.com/app@sha256:1a2b3c...
6.3 兼容性与 fallback 方案
不是所有 registry 都实现了 referrers API。当服务端不支持时,客户端回退到 tag scheme:把附属工件存成 sha256-<被引用 digest> 这个 tag。这就是为什么你会看到 sha256-abc...sig、sha256-abc...sbom 这类奇怪 tag。
| registry | referrers API 支持 | 备注 |
|---|---|---|
| Docker Hub | 支持(较新) | 早期不支持,靠 tag fallback |
| Harbor | 2.8+ 支持 | 需注意版本与 OCI artifact 开关 |
| GHCR | 支持 | 对 OCI 1.1 跟进较早 |
| AWS ECR | 部分支持 | 早期仅 tag fallback,版本差异大 |
一句话:referrers 让“签名/SBOM 跟着镜像走”成为协议级能力,但在混合 registry 环境下,客户端必须同时实现 referrers 查询与 tag fallback 两条路径。
7. 把非镜像工件推入 registry
7.1 Helm chart
# Helm 3.8+ 原生支持 OCI registry
helm package ./mychart
helm push mychart-0.1.0.tgz oci://registry.example.com/charts
helm pull oci://registry.example.com/charts/mychart --version 0.1.0
Helm chart 被存成一个 OCI manifest,config 是 application/vnd.cncf.helm.config.v1+json,chart tgz 作为 layer。
7.2 SBOM 与签名
# 生成 SBOM 并以 subject 关联到镜像
syft registry.example.com/app@sha256:1a2b3c... -o cyclonedx-json > sbom.json
cosign attach sbom --sbom sbom.json registry.example.com/app@sha256:1a2b3c...
cosign sign --yes registry.example.com/app@sha256:1a2b3c...
cosign attach 生成的工件通过 subject 指向镜像,正是 6.2 节 referrers API 的典型用法。
7.3 通用工具 oras
# 推一个 WASM 模块作为 OCI 工件
oras push registry.example.com/plugins/hello:1.0 \
--artifact-type application/vnd.wasm.content.layer.v1+wasm \
hello.wasm:application/wasm
# 推一个 OPA 策略包
oras push registry.example.com/policies/baseline:1.0 \
--artifact-type application/vnd.cncf.openpolicyagent.policy.layer.v1+rego \
policy.rego:application/vnd.cncf.openpolicyagent.policy.layer.v1+rego
7.4 工件类型与工具对照
| 工件 | 常用工具 | artifactType 示例 |
|---|---|---|
| Helm chart | helm push | application/vnd.cncf.helm.config.v1+json |
| SBOM | cosign attach sbom | application/vnd.cyclonedx+json |
| 签名 | cosign sign | application/vnd.dev.cosign.simplesigning.v1+json |
| WASM 模块 | oras push | application/vnd.wasm.content.layer.v1+wasm |
| 策略包(Rego) | oras push | application/vnd.cncf.openpolicyagent.policy.layer.v1+rego |
统一用 OCI registry 存工件,好处是复用同一套认证、复制、GC、审计与签名体系——你不再需要为 chart、SBOM、WASM 各维护一个存储系统。
8. OCI layout、save/load/export 与跨仓库搬运
8.1 OCI layout:磁盘上的镜像
OCI Image Layout 是一个目录结构,把 registry 里的内容“落盘”成可搬运的文件:
oci-layout # {"imageLayoutVersion": "1.0.0"}
index.json # 顶层 index,指向 manifest
blobs/sha256/ # 所有 blob,文件名即摘要
# 从 registry 导出为 OCI layout
skopeo copy docker://nginx:1.27 oci:./nginx-oci:latest
# 从 OCI layout 导入
skopeo copy oci:./nginx-oci:latest docker://registry.example.com/nginx:1.27
8.2 save / load / export 的区别
| 命令 | 产物 | 是否保留元数据 | 适用场景 |
|---|---|---|---|
| docker save | tar(Docker 自有格式) | 保留层、tag、历史 | 离线搬运镜像 |
| docker load | 从 save 的 tar 恢复 | 保留 | 离线导入 |
| docker export | 容器根文件系统 tar | 不保留层与历史 | 导出运行中容器快照 |
docker export 导出的是扁平化后的文件系统,没有层、没有 config、没有历史——它产出的东西根本不是一个 OCI 镜像,别再导回去当镜像用。
8.3 skopeo copy:registry 到 registry
# 直接在两个 registry 之间搬运,不经本地 docker daemon
skopeo copy --all \
docker://docker.io/library/nginx:1.27 \
docker://registry.example.com/mirror/nginx:1.27
--all 表示搬运整个 index(所有平台);不加则只搬当前平台的 manifest。镜像镜像(mirror)场景下必须加 --all,否则同步出来的多架构镜像会“缺胳膊少腿”。
8.4 registry 兼容性差异
| 能力 | Docker Hub | Harbor | GHCR | ECR |
|---|---|---|---|---|
| OCI 1.1 manifest | 支持 | 支持 | 支持 | 较新版本支持 |
| referrers API | 支持 | 2.8+ | 支持 | 版本相关 |
| zstd 层 | 支持 | 支持 | 支持 | 版本相关 |
| OCI layout 导入 | 不适用 | 支持 | 不适用 | 不适用 |
8.5 生产清单与踩坑表
生产落地五条纪律:
- 部署一律用
@sha256:digest 引用,不用浮动 tag; - CI 产出 SBOM 与签名后,用
cosign attach/oras attach以 subject 关联; - 跨 registry 同步统一走
skopeo copy --all,保留多架构; - 定期用
crane digest校验线上 tag 是否仍指向预期 digest(防 tag 漂移); - registry 升级前确认 referrers API 与 OCI 1.1 支持情况。
| 现象 | 根因 | 处置 |
|---|---|---|
| no matching manifest for linux/arm64 | index 里没有该平台 | 用 buildx 重推并带 –platform |
| referrers 查询返回空 | registry 不支持 referrers API | 回退 tag scheme 查询 |
| digest 与预期不符 | tag 被重新推送覆盖 | 改用 digest 引用并加漂移检测 |
| skopeo copy 后少了平台 | 未加 –all | 加 –all 重搬 |
| zstd 层拉取失败 | 运行时或 registry 不支持 zstd | 改用 gzip 层或升级组件 |
| 签名校验报 subject 不匹配 | 签名针对旧 digest | 重新对当前 digest 签名 |
9. 总结
| 主题 | 关键结论 | 一句话记忆 |
|---|---|---|
| 三大组件 | manifest 引用 config 与 layers,全用 digest 钉死 | 目录、元数据、内容三层引用 |
| media type | OCI 与 Docker schema2 类型名不同,zstd 仅 OCI 有 | 类型写错,运行必炸 |
| image index | 多架构靠 index 分发,客户端按平台选择 | 一个 tag,多个平台 |
| diff_ids | 压缩摘要与解压摘要两条校验链 | 压缩解压双重指纹 |
| OCI Artifacts | artifactType 声明类型,subject 声明归属 | 工件跟着镜像走 |
| referrers API | 查询附属工件,不支持的 registry 回退 tag scheme | 两条查询路径都要实现 |
| 工件生态 | Helm/SBOM/签名/WASM/策略包统一进 registry | 复用同一套仓库能力 |
| OCI layout | 磁盘上的可搬运镜像结构,blobs 以摘要命名 | 落盘的 registry 切片 |
| 不可变性 | tag 可变、digest 不可变 | 生产一律用 digest |
OCI 规范的价值,远不止“让 Docker 能拉镜像”。它把内容寻址、可组合的 descriptor、可扩展的工件类型三件事标准化,使 registry 成为一个通用的、可签名的、可复制的制品中枢。理解 manifest、index、config 与 artifact 四层结构后,你会发现:多架构构建、供应链签名、SBOM 关联、Helm chart 分发,本质都是同一套机制的复用。工程上真正要落地的,是两条纪律——用 digest 而非 tag 做不可变引用,以及在混合 registry 环境下同时兼容 referrers API 与 tag fallback。把这两条守住,OCI 生态的绝大多数“玄学问题”都会变成可解释、可排查的结构问题。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。