Docker push 报 unknown manifest class:阿里云 ACR 个人版兼容问题

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处
DockerBuildKitOCI阿里云 ACR

我遇到这个错误时,前一天用的还是同一份构建脚本。Docker 镜像构建成功,绝大多数层也显示 Already exists,偏偏在最后一步失败:

The push refers to repository [REGISTRY/OWNER/IMAGE]
f93a84eb78fe: Already exists
44136fa355b3: Already exists
08d42cdba493: Layer already exists
...
error from registry: unknown manifest class for application/vnd.oci.empty.v1+json

一开始我怀疑过登录状态、网络和镜像层损坏。逐项检查后才发现,真正出问题的是 Docker 升级后的默认构建格式:BuildKit 给镜像附加了 provenance,并按 OCI Artifact 格式保存。我的目标仓库是阿里云 ACR 个人版,它接收了镜像层,却拒绝了 provenance 使用的空 OCI config。

先恢复推送

如果使用的是阿里云 ACR 个人版,可以在构建时关闭 provenance 和 SBOM:

docker build \
  --provenance=false \
  --sbom=false \
  -t REGISTRY/OWNER/IMAGE:TAG \
  -f Dockerfile \
  .

docker push REGISTRY/OWNER/IMAGE:TAG

PowerShell 脚本同样把参数加在 docker build 上:

docker build --provenance=false --sbom=false -t $image -f Dockerfile .
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

docker push $image
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

必须重新构建。已经生成的本地 OCI image index 不会因为重新执行 docker push 而变回普通镜像 manifest。

这次错误由 provenance 引起,单独使用 --provenance=false 已经能解决。脚本里同时写上 --sbom=false,是为了明确这条发布链路不上传任何 attestation,免得以后启用 SBOM 时再次碰到同类问题。

为什么镜像层都成功了,推送还是失败

docker push 不只上传文件层。仓库还要接收 manifest,并在最后把 tag 指向它。普通单平台镜像的结构大致如下:

image manifest
├── image config
└── filesystem layers

带 provenance 的构建结果外面还有一层 image index:

image index
├── linux/amd64 image manifest
└── provenance attestation manifest

所以,Layer already exists 只能说明 blob 已经进了仓库。只要 attestation manifest 或最外层 image index 被拒绝,tag 就不会更新。这也解释了为什么反复推送时,前面几乎全是 Already exists,最后却总在同一个位置报错。

provenance 是什么

Docker 的 Build attestations 文档把 provenance 定义为构建来源证明,其中记录镜像由什么构建器、代码版本和材料生成。接收方可以用它核对产物来源,SLSA 等供应链验证也会用到这些信息。

它不参与容器运行。关闭 provenance 不会删掉应用文件,也不会改变镜像的入口命令;少掉的是构建追踪信息。

SBOM 是另一类 attestation。它记录镜像中包含的软件包和组件,解决的是“镜像里有什么”;provenance 记录“镜像怎么构建”。这两个参数可以分别控制:

--provenance=false
--sbom=false

三次变化叠在了一起

我最初把这个问题理解成“Docker 最近新增了一个奇怪格式”。查完版本记录后才发现,provenance、OCI 空描述符和新的默认封装方式并不是同时出现的。

2023 年 1 月:BuildKit 开始默认生成 provenance

BuildKit v0.11.0Buildx v0.10.0 都发布于 2023 年 1 月 10 日。Docker Build variables 文档说明,从 BuildKit v0.11 开始,镜像默认包含最小级别的 provenance attestation。

从这时起,同一条 docker build 命令就可能同时产出运行镜像和构建证明。一些仓库随后出现无标签小镜像、旧客户端无法解析 manifest 等兼容性问题。

2024 年 2 月:OCI 1.1 定义空 JSON 描述符

OCI 1.1 发布说明记录了 Image Specification 1.1.0 和 Distribution Specification 1.1.0 在 2024 年 2 月 15 日发布。这一版加入了 subjectartifactType 和 Referrers API,用于把签名、SBOM、provenance 等制品关联到镜像。

规范还定义了下面这个空描述符:

{
  "mediaType": "application/vnd.oci.empty.v1+json",
  "digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
  "size": 2,
  "data": "e30="
}

OCI Image Manifest 对 Empty Descriptor 的说明显示,它的内容只是 {},SHA-256 固定以 44136fa355b3 开头。推送日志中的那一项不是损坏的镜像层,而是一个标准占位符:这个 Artifact 不需要普通容器镜像那样的 config,但 manifest 结构仍要保留 config 字段。

2026 年 7 月:BuildKit 改用 OCI Artifact 保存 attestation

BuildKit v0.32.0 发布于 2026 年 7 月 29 日。Docker Build variables 文档说明,从这一版开始,启用 OCI media types 时,BuildKit 默认把 attestation 存成 OCI Artifact。Docker Engine 29.7.0 发布说明则记录了内置 BuildKit 升级到 v0.32.0。

Buildx v0.36.1 发布说明显示,这一版于 2026 年 8 月 4 日发布,把 BuildKit 依赖更新到 v0.32.2,并增加环境变量 BUILDX_NO_DEFAULT_OCI_ARTIFACT,用于关闭新的默认存储方式。

Provenance 早在 2023 年就有了。这次突然报错,是因为 2026 年的 BuildKit 改了默认封装格式,而 ACR 个人版没有接住这个变化。

问题出在 ACR 个人版的支持范围

阿里云 ACR 术语文档列出了两类仓库地址:

个人版:registry.cn-REGION.aliyuncs.com/NAMESPACE/REPOSITORY:TAG
企业版:INSTANCE-registry.cn-REGION.cr.aliyuncs.com/NAMESPACE/REPOSITORY:TAG

我推送的地址是 registry.cn-hangzhou.aliyuncs.com/...,属于 ACR 个人版。

阿里云 ACR 产品说明对两个版本的功能描述并不相同。个人版提供基础镜像托管、构建和授权;OCI 制品托管写在企业版能力中。企业版 OCI 1.1 使用说明还写明,从 2024 年 4 月起,新创建的企业版实例支持 OCI Image 和 Distribution 1.1,可以关联签名、SBOM 等衍生制品。

阿里云企业版 Skill 制品文档直接把 application/vnd.oci.empty.v1+json 列为 Config Media Type。这说明不能把问题写成“阿里云完全不支持 OCI Artifact”。企业版有对应能力;我实际使用的个人版仓库在推送时返回 unknown manifest class,说明它当前不能接收 BuildKit v0.32 生成的这类 provenance manifest。

换句话说,Docker 生成的格式符合 OCI 规范,ACR 个人版也能保存普通容器镜像,但它没有企业版的 OCI Artifact 能力。两边各自都能正常工作,碰到一起才报错。

如何确认是同一个问题

先看当前版本:

docker version
docker buildx version
docker buildx ls

然后检查本地 tag:

docker image inspect REGISTRY/OWNER/IMAGE:TAG --format '{{json .Descriptor}}'

如果 mediaType 是下面这个值,本地 tag 指向的是 OCI image index:

application/vnd.oci.image.index.v1+json

新版 Buildx 还能查看构建附件:

docker buildx history ls
docker buildx history inspect BUILD_REF

Attachments 中同时出现平台镜像和 https://slsa.dev/provenance/v1,说明 provenance 已经挂在 index 里。

最后看远程 tag:

docker buildx imagetools inspect REGISTRY/OWNER/IMAGE:TAG

我当时看到的远程 tag 仍是旧的 application/vnd.docker.distribution.manifest.v2+json,创建时间也没有更新;本地新镜像则是 application/vnd.oci.image.index.v1+json。这个对比把问题缩小到了 manifest 格式,而不是镜像内容。

平时常用的镜像检查和清理命令,我另外整理在《Docker 常用命令速查》中。如果问题是应用层频繁失效、每次都要重传大文件,可以再看《Next.js standalone Docker 镜像分层优化》

如果仍想保留 provenance

Buildx v0.36.1 提供了一个过渡开关:

$env:BUILDX_NO_DEFAULT_OCI_ARTIFACT = "1"

它会让 Buildx 不再默认使用新的 OCI Artifact 存储方式,但 provenance 仍然存在。旧格式是否能推到目标仓库,要实际测试;ACR 个人版并没有在官方文档中承诺支持这类 attestation。

如果发布流程需要签名、SBOM 或 SLSA 验证,关闭 provenance 只能算兼容处理。更稳妥的选择是使用支持 OCI 1.1 Artifact 的企业版实例或其他仓库,把构建证明保留下来。

只想给个人版推送可运行镜像时,--provenance=false --sbom=false 更直接。选择哪种方式,取决于这条发布链路是否真的在消费这些证明。

为什么前一天还能推送

脚本没变,不代表构建结果的封装格式没变。docker build 背后还有 Docker Engine、Buildx、BuildKit、builder driver、image store 和 exporter。

我检查时使用的是 Docker Engine 29.7.2、Buildx 0.36.1 和 BuildKit 0.32.2,daemon 还启用了 containerd snapshotter。containerd image store 能保存 image index 和 attestation,旧的 classic image store 做不到。版本、builder 或 image store 只要有一项变化,同一份脚本就可能生成另一种 manifest。

以后再遇到“昨天能推,今天不能推”,我会先比较这些信息:

  • Docker Engine、Buildx 和 BuildKit 版本;
  • 当前 builder、driver 和 image store;
  • 本地与远程 tag 的 media type;
  • 构建历史中有没有 provenance 或 SBOM。

这比反复清缓存、重新登录或者重推同一个 tag 快得多。镜像层全部成功但 tag 没更新时,先查 manifest。