OCI 镜像与工件规范:manifest、index 与 artifact 生态

系统剖析 OCI Image Spec 与 Artifacts 规范:manifest、config、layers 三大组件与 media type、image index 与多架构、artifactType 与 referrers API,把 Helm chart、SBOM、签名作为 OCI 工件推送,并覆盖 OCI layout 与 registry 兼容性差异。

前置阅读:建议先阅读 镜像签名与供应链验证: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 typeDocker 等价
单平台 manifestapplication/vnd.oci.image.manifest.v1+jsonapplication/vnd.docker.distribution.manifest.v2+json
多平台 indexapplication/vnd.oci.image.index.v1+jsonapplication/vnd.docker.distribution.manifest.list.v2+json
镜像 configapplication/vnd.oci.image.config.v1+jsonapplication/vnd.docker.container.image.v1+json
tar+gzip 层application/vnd.oci.image.layer.v1.tar+gzipapplication/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 v1Docker 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 indexDocker manifest list
media typevnd.oci.image.index.v1+jsonvnd.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。

registryreferrers API 支持备注
Docker Hub支持(较新)早期不支持,靠 tag fallback
Harbor2.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 charthelm pushapplication/vnd.cncf.helm.config.v1+json
SBOMcosign attach sbomapplication/vnd.cyclonedx+json
签名cosign signapplication/vnd.dev.cosign.simplesigning.v1+json
WASM 模块oras pushapplication/vnd.wasm.content.layer.v1+wasm
策略包(Rego)oras pushapplication/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 savetar(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 HubHarborGHCRECR
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/arm64index 里没有该平台用 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 typeOCI 与 Docker schema2 类型名不同,zstd 仅 OCI 有类型写错,运行必炸
image index多架构靠 index 分发,客户端按平台选择一个 tag,多个平台
diff_ids压缩摘要与解压摘要两条校验链压缩解压双重指纹
OCI ArtifactsartifactType 声明类型,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 生态的绝大多数“玄学问题”都会变成可解释、可排查的结构问题。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「docker」更多文章

  1. 容器 CPU 调度与 NUMA:绑核、实时性与 QoS 保障
  2. Docker Daemon 运维:systemd 集成、配置调优与日志治理
  3. 构建缓存进阶:Buildx 远程缓存、CI 加速与缓存失效