結論先説:問題不在 Harbor,而在 CI 裏用的 moby/buildkit:buildx-stable-1 太老了
本地 Docker 自帶的 BuildKit 已經升級,可以和 Harbor v2.14 正常協作;
CI 使用的是舊版 BuildKit 容器鏡像,生成的緩存 manifest 與 Harbor 的實現不兼容,最終導致 404。


背景:CI 構建失敗,但鏡像其實已經推成功了

GitHub Actions 中的構建日誌大致如下:

#29 exporting to image
#29 pushing manifest for harbor.xxxx.com/cdtgroup/enterprise-back-tone:dev ... done
#29 pushing manifest for harbor.xxxx.com/cdtgroup/enterprise-back-tone:dev-589ee60 ... done
#29 DONE 21.8s

#31 exporting cache to registry
#31 writing cache manifest sha256:d0124cd34bba05d5...
#31 ERROR: error writing manifest blob: failed commit on ref "sha256:d0124cd34bba05d5...":
unexpected status from PUT request to https://harbor.xxxx.com/v2/cdtgroup/enterprise-back-tone/manifests/buildcache: 404 Not Found
...
::error::buildx failed with: ERROR: failed to build: failed to solve: error writing manifest blob ...

可以看到:

  • 鏡像 :dev:dev-589ee60 已經成功推送;
  • 報錯發生在 “exporting cache to registry” 這一步,對 :buildcache 這個 tag 寫 manifest 時收到了 404。

對應的 GitHub Actions workflow 配置(簡化):

- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    context: .
    file: docker/Dockerfile
    platforms: linux/amd64
    push: true
    tags: ${{ steps.meta-dev.outputs.tags }}
    cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
    cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

很典型的 BuildKit registry cache 配置。


本地完全相同的構建命令,卻一切正常

在本地,使用的命令與 CI 配置幾乎一模一樣:

docker builder build \
  --build-arg VERSION=dev \
  --build-arg BUILDKIT_INLINE_CACHE=1 \
  --cache-from type=registry,ref=harbor.xxxx.com/cdtgroup/enterprise-back-tone:buildcache \
  --cache-to type=registry,ref=harbor.xxxx.com/cdtgroup/enterprise-back-tone:buildcache,mode=max \
  --file docker/Dockerfile \
  --platform linux/amd64 \
  --tag harbor.xxxx.com/cdtgroup/enterprise-back-tone:dev \
  --tag harbor.xxxx.com/cdtgroup/enterprise-back-tone:dev-791af6f \
  --push .

現象:

  • 鏡像可以推;
  • buildcache 也可以正常生成並推送;
  • Harbor UI 中能看到緩存 tag。

Harbor 版本為:

v2.14.0-44a74424

查官方文檔可以確認:Harbor 2.x 已經是標準的 OCI Registry,支持各種 OCI artifact,不存在「完全不支持 BuildKit 緩存」這一説。

那為什麼 同一個 Harbor、同一個倉庫、同一套 cache 配置,本地和 CI 的結果完全不同?


排查思路:先排除掉“直覺上最可疑”的方向

第一反應通常會懷疑:

  • 權限問題(token 沒有 push 權限?)
  • 倉庫不存在(項目沒創建?)
  • Harbor 不支持這種 manifest mediaType?

但這些都可以通過現象排除:

  • 鏡像 tag 已經成功 push,説明:
  • 倉庫存在;
  • 憑證有效,具備 push 權限。
  • 404 出現在 PUT /manifests/buildcache,而不是 /manifests/dev/manifests/dev-xxx
  • 本地對同一 Harbor 成功使用 registry cache,説明 Harbor 對這種 artifact 是支持的。

真正可疑的,只剩下一個:CI 使用的 BuildKit 跟本地不是一回事。


關鍵差異:CI 使用的是自建的老 BuildKit 容器

看 CI 的 Buildx 配置:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
  with:
    driver-opts: image=harbor.xxxx.com/library/moby/buildkit:buildx-stable-1

這裏有兩個關鍵信息:

  • Buildx 的 builder driver 是 docker-container
  • 具體運行的 buildkitd 鏡像來自 私有 Harbor:harbor.xxxx.com/library/moby/buildkit:buildx-stable-1

這通常意味着:

  • 這份鏡像是很早之前從官方 moby/buildkit:buildx-stable-1 同步過去的;
  • 長期沒更新,版本明顯落後於你本機 Docker 附帶的 BuildKit

而你本地的 docker builder build

  • 用的是 Docker 安裝自帶的 較新 BuildKit;
  • 即使命令行選項一樣,底層生成的 cache manifest 格式也可能與舊版本不同。

BuildKit registry cache 背後的關鍵點

根據 BuildKit 官方文檔(簡化要點):

  • cache-to type=registry 會把構建緩存以 獨立的 cache manifest 推到 registry;
  • cache-from type=registry 則通過拉取並解析這份 cache manifest 來命中緩存;
  • 這份 manifest 的 mediaType 不是普通的 Docker Image Manifest,而是符合 OCI 規範的特殊類型。

Harbor 2.14 的特點:

  • 完整支持 OCI 鏡像、manifest list、以及更通用的 OCI artifact;
  • 但對某些「老版本、非標準實現」的 payload,依然可能直接返回 404 或 400。

因此,當 CI 使用舊版 moby/buildkit 往 Harbor 推 cache manifest 時,極有可能出現:

  • manifest 的結構或 header 與最新規範不一致;
  • Harbor 在校驗時認為該 manifest 不合法;
  • 最終對 PUT /v2/.../manifests/buildcache 返回 404;
  • Buildx 日誌就出現了你看到的那條錯誤。

相反,本地的新 BuildKit 早已修過這些兼容問題,所以完全正常。

這一點也解釋了:為什麼是 404 而不是 401/403——權限與路徑都沒問題,只是 Harbor 不接受這個「長得怪怪的」manifest。


最終根因:CI 使用的 moby/buildkit:buildx-stable-1 太老

總結一下這次問題的成因鏈路:

  1. CI 中通過 setup-buildx-action 使用了自建的 BuildKit 鏡像:
driver-opts: image=harbor.xxxx.com/library/moby/buildkit:buildx-stable-1
  1. 這份鏡像是早年同步的,版本遠落後於當前 Docker / BuildKit 官方版本。
  2. 老版本 BuildKit 在使用 cache-to type=registry 時:
  • 生成的 cache manifest 格式或 header 存在兼容性問題;
  • 在 Harbor v2.14 上被判為非法/不支持,返回 404。
  1. 本地構建則使用了更新的 BuildKit:
  • 同樣的 Harbor + 同樣的 cache 配置,可以順利推送 cache;
  • 説明 Harbor 本身沒有問題。

解決方案與改進建議

圍繞這個根因,有幾種可選路徑(由易到難):

1. 更新 CI 中的 BuildKit 鏡像(推薦)

目標是讓 CI 使用與本地相同代際的 BuildKit。

  • 在任意能訪問 Docker Hub 的環境執行:
docker pull moby/buildkit:buildx-stable-1
docker tag moby/buildkit:buildx-stable-1 harbor.xxxx.com/library/moby/buildkit:buildx-stable-1
docker push harbor.xxxx.com/library/moby/buildkit:buildx-stable-1
  • 不改 workflow 內容,下次 CI 跑起來時,buildkitd 實際已經是最新版,對 Harbor 的行為會和本地一致。

或者更簡單一點:

  • 直接在 workflow 中去掉 driver-opts,讓 setup-buildx-action 使用官方推薦的默認 builder 鏡像。

2. 調整緩存策略為 inline,規避 registry cache 路徑

如果短期內不方便改 CI 運行環境,可以先把緩存方式改為更保守的 inline

cache-to: type=inline
# 需要時再加:
# cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev

特點:

  • 緩存信息嵌在鏡像 config 中,通過正常鏡像 manifest 流程走;
  • Harbor 無須理解額外的 cache manifest,兼容性更好;
  • 缺點是緩存粒度比 mode=max + registry 低一些,但夠用且穩定。

3. 臨時關閉 cache,先恢復流水線

如果當前構建頻率不高、時間可接受,想先讓 CI 穩定跑完:

  • 直接註釋或刪除所有 cache-from / cache-to 配置;
  • 鏡像構建與推送照常,只是少了構建緩存加速。

小結與經驗教訓

這次問題表面上是:

Harbor 報 404,似乎「不支持 BuildKit 構建緩存」。

實質上卻是:

CI 和本地使用了兩套完全不同版本的 BuildKit
導致同一個 Harbor 上的行為不一致。

從中可以抽象出幾條經驗:

  • 遇到「本地 OK,CI 掛」的情況時,要優先對比:
  • Docker / Buildx / BuildKit 版本;
  • Registry 鏡像是否是舊的內部鏡像。
  • 使用自建 BuildKit 鏡像時,要有 升級策略,否則遲早會遇到與上游生態(Harbor、Docker Hub 等)的兼容問題。
  • 對於 BuildKit 緩存,inline 是更保守、兼容性更好的選擇,可以作為默認方案;只有性能有較高要求、環境可控時再使用 type=registry, mode=max