結論先説:問題不在 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 太老
總結一下這次問題的成因鏈路:
- CI 中通過
setup-buildx-action使用了自建的 BuildKit 鏡像:
driver-opts: image=harbor.xxxx.com/library/moby/buildkit:buildx-stable-1
- 這份鏡像是早年同步的,版本遠落後於當前 Docker / BuildKit 官方版本。
- 老版本 BuildKit 在使用
cache-to type=registry時:
- 生成的 cache manifest 格式或 header 存在兼容性問題;
- 在 Harbor v2.14 上被判為非法/不支持,返回 404。
- 本地構建則使用了更新的 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。