docker buildx 异构打包功能支持#1076
现有的方案如果要开启多平台构建,需要开root模式,但开了就意味着着用户可以直接操控母机,有巨大的安全问题。所以没法开启
目前内部版本正在验证一个新的方案,通过开启containerd-snapshotter特性,实现rootless模式下支持模拟多平台构建。但开启这个特效同时需要环境docker27+, cgroup v2. 与我们现有的一些配套解决方案存在兼容性问题。可能会导致部分流水线执行失败。目前还在解决中。
大家可以关注这个issue, 获得最新的进展。
号外: docker buildx 异构打包功能开放公测啦!
目前通过流水线中指定 runner.tags: cnb:arch:amd64:containerd-snapshotter,可以将构建指定到支持containerd-snapshotter特性的机器(ps. 未来这个标签会去掉,仅供测试,请勿在正式环境中使用)
具体使用方法可参考: https://cnb.cool/loviselu/docker-buildx-multi-platform-example
未来计划:等整个流程跑通没啥问题,后续会把平台所有机器都开启这个特性,无需大家单独指定tag
哈?他们就没动原来的架构,都还在bate吧?只是 runner.tags: cnb:arch:amd64:containerd-snapshotter 的时候是新的
刚打了个包,arm64的,正常的呀
web_trigger_one:
- runner:
tags: cnb:arch:amd64
services:
- docker
env:
IMAGE_TAG: ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}:latest-linux-amd64
stages:
- name: docker build
script: docker build -t ${IMAGE_TAG} .ide
- name: docker push
script: docker push ${IMAGE_TAG}
- name: resolve
type: cnb:resolve
options:
key: build-amd64
- runner:
tags: cnb:arch:arm64:v8
services:
- docker
env:
IMAGE_TAG: ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}:latest-linux-arm64
stages:
- name: docker build
script: docker build -t ${IMAGE_TAG} .ide
- name: docker push
script: docker push ${IMAGE_TAG}
- name: resolve
type: cnb:resolve
options:
key: build-arm64
- services:
- docker
env:
IMAGE_TAG: ${CNB_DOCKER_REGISTRY}/${CNB_REPO_SLUG_LOWERCASE}:latest
stages:
- name: await the amd64
type: cnb:await
options:
key: build-amd64
- name: await the arm64
type: cnb:await
options:
key: build-arm64
- name: manifest
image: cnbcool/manifest
settings:
target: ${IMAGE_TAG}
template: ${IMAGE_TAG}-OS-ARCH
platforms:
- linux/amd64
- linux/arm64
- name: remove tag
type: artifact:remove-tag
options:
name: ${CNB_REPO_NAME}
tags:
- latest-linux-amd64
- latest-linux-arm64
type: docker
cnbcool/manifest 缺少增量模式,单架构 Debug 发布会覆盖线上其他架构提交目的:阐明在 CNB 制品库升级为 OCI multi‑index 之后,60 个仓库(大部分复用同一套流水线)在「只更新单一 CPU 架构」时会遭遇的破坏性问题,并说明现有插件功能与需求缺口,便于官方评估并实现“增量(append)模式”。本 Issue 仅聚焦 单架构增量发布 场景——双架构并行一次性发布目前运转正常,不在讨论范围。
| 说明 | |
|---|---|
| 流水线规模 | 60 个仓库大多复用同一 CI 模板,部分还有每日定时构建。 |
| 架构矩阵 | 主流生产平台:linux/amd64 与 linux/arm64; |
| 常见开发节奏 | 90 % 的功能/缺陷修复只影响 AMD64;Arm64 需继续拉取稳定旧版镜像。 |
| 旧时代运行良好的策略 | 制品库曾使用 单列 manifest 格式,Shell 通过 docker manifest create --amend 增量加入新架构条目,未触及其他架构。 |
| 关键环境升级 | 2025‑07:制品库切换到 OCI multi‑index list; Runner 升级 Docker 27 + containerd‑snapshotter(rootless)。 |
旧 Shell 工作流程(示意)
# 1️⃣ 构建并推送本次变更的单架构镜像,例如 latest‑amd64
docker build -t $REG/foo/bar:latest-amd64 .
docker push $REG/foo/bar:latest-amd64
# 2️⃣ 探测另一架构镜像是否已存在
if docker manifest inspect $REG/foo/bar:latest-arm64 >/dev/null 2>&1; then
OTHER_PLATFORM_EXISTS=true
fi
# 3️⃣ 增量 or 单架构策略
if [ "$OTHER_PLATFORM_EXISTS" = true ]; then
docker manifest create --amend $REG/foo/bar:latest \
$REG/foo/bar:latest-amd64 \
$REG/foo/bar:latest-arm64
docker manifest push --purge $REG/foo/bar:latest
else
docker tag $REG/foo/bar:latest-amd64 $REG/foo/bar:latest
docker push $REG/foo/bar:latest
fi
运行结果: 线上 docker pull foo/bar:latest 始终可拉到两架构镜像。
| 维度 | 文档列明的插件能力 | 业务需求 | 差距 |
|---|---|---|---|
| 合并方式 | 传入 template + platforms → 一次性 docker manifest create && push | 增量合并:读取已存在 manifest,追加或替换当前架构条目,其余保持 | ❌ 缺失增量流程 |
| 缺失镜像处理 | ignoreMissing=true 时不报错,但产出 list 只含现有镜像 | 未构建架构必须沿用旧版本 | ❌ 无法保留旧平台 |
| 细粒度注解 | 无 variant / digest 参数 | 需要能标注 --variant v8 等 | ❌ 功能缺失 |
| CLI 依赖 | 使用 Docker CLI manifest 子命令 (Experimental) | 需跟随官方路线迁移至 Buildx imagetools | ⚠️ 未来可能失效 |
| 多 target 支持 | 可一次推送 latest, v1.0.0 等多个 tag | 已满足 | ✅ |
foo/bar:latest-arm64(上一版 Arm64 镜像)。foo/bar:latest-amd64。- stage: manifest
image: cnbcool/manifest
settings:
target: foo/bar:latest
template: foo/bar:latest-OS-ARCH
platforms:
- linux/amd64 # ⚠️ 本轮只列 amd64
latest tag 继续同时包含 amd64 + arm64。docker pull foo/bar:latest → manifest unknown (404)。• 函数 GetManifests 只遍历传入的 platforms,为每个元素生成一条
ManifestEntry,没有任何逻辑去读取旧 manifest:
https://cnb.cool/cnb/plugins/cnbcool/manifest/-/blob/main/utils/args.go#L58-82
• 生成完的 manifests 列表随后被封装成 YAMLInput 并传给
registry.PushManifestList,该 API 直接覆盖目标 tag 的旧 manifest,
不做增量合并:
https://cnb.cool/cnb/plugins/cnbcool/manifest/-/blob/main/utils/args.go#L31-46
→ 结论:当 platforms 只写 amd64 时,新 manifest list 只包含 amd64,
旧的 arm64 子镜像被覆盖,Arm 机器拉取失败。
-------------------- 代码片段解读 --------------------
func GetYamlInputs(args *Args) ([]types.YAMLInput, error) {
// Modified from https://github.com/estesp/manifest-tool/…/push.go
inputs := []types.YAMLInput{}
targets := strings.Split(args.Target, ",") // ❶ 允许同时处理多个 target
for _, target := range targets {
manifests, err := GetManifests(args.Platforms, args.Template) // ❷
if err != nil { return inputs, err }
input := types.YAMLInput{ // ❸ 构造单个 YAMLInput
Image: target, // *Image* 字段就是要推送的 tag
Manifests: manifests, // ManifestEntry 列表
Tags: nil, // 没用到 tag alias 功能
}
inputs = append(inputs, input) // ❹ 累加到返回切片
}
return inputs, nil
}
────────────────────────────────────────────────────────
解释关键点
targets 处理
args.Target 可以是逗号分隔的多个 tag;函数会为每个 tag 构造
一个独立的 YAMLInput,最终一次推送到多个目标。
(示例 YAML 里的 foo/bar:v1.0.0,foo/bar:latest 就在这里拆分)
GetManifests 调用
核心决定权 全在 GetManifests(args.Platforms, args.Template):
• 仅根据 调用方传入的 platforms 生成 ManifestEntry 列表;
• 不读取远端旧 manifest,也没有追加逻辑。
因此如果 platforms 只包含 linux/amd64,返回列表就只有 amd64。
YAMLInput 结构
manifest-tool 会把 YAMLInput 里的 Manifests 直接写进新的
manifest list,然后推送到 Image 指定的 tag。旧 manifest
条目(例如 arm64)不会被合并进来,而是被覆盖掉。
与复现步骤对应
当你在 settings 里只写 platforms: [linux/amd64]:
• GetManifests → 只生成 amd64 条目
• registry.PushManifestList → 将仅含 amd64 的 list 覆盖到
foo/bar:latest
• Arm64 节点 pull latest ⇒ 404.
结论:这段代码正是“只要 platforms 缺少某平台,就会丢失该平台”
的直接根源,也解释了复现步骤中的实际表现。
| 编号 | 风险点(与历史 Shell 逻辑直接冲突) | 触发条件 | 结果 |
|---|---|---|---|
| A | Shell 正则依赖 config/layers 字段来判断“是否单架构 manifest” | <tag-arch> 被误推成 OCI index | Shell 误判“镜像不存在”,走单架构直推流程,导致线上另一平台镜像被删除 |
mode: append — 当目标 tag 已存在时,插件应先
manifest inspect 旧 list,再追加 / 覆盖当前架构条目并推送,
未列架构保持不变。
↳ 官方等价 CLI:docker buildx imagetools create --append …
↳ 证据链:Docker 文档 “Append new sources to an existing manifest list”
https://docs.docker.com/reference/cli/docker/buildx/imagetools/create/#append-new-sources-to-an-existing-manifest-list---append
variantMap — 允许在 settings 中为各平台附加 variant、显式
digest 等元数据,以满足 arm64/v8 等精细场景。
↳ 官方等价 CLI:docker manifest annotate … --variant v8 …
↳ 证据链:Docker 文档 docker manifest annotate 支持 --variant
https://docs.docker.com/reference/cli/docker/manifest/annotate/
↳ 插件目前 无此字段:README 仅列出 username / password / target / template / platforms / skipVerify / ignoreMissing
https://cnb.cool/cnb/plugins/market/-/blob/b7329821a3a2debd403ac51015c3772764d293e5/plugins/cnbcool/manifest/README.md
只要实现
mode: append,我就能删除复杂 Shell,让所有这些仓库通过一行 YAML 安全完成单架构迭代,并随时扩展更多架构。

cnbcool/manifest缺少增量模式,单架构 Debug 发布会覆盖线上其他架构提交目的:阐明在 CNB 制品库升级为 OCI multi‑index 之后,60 个仓库(大部分复用同一套流水线)在「只更新单一 CPU 架构」时会遭遇的破坏性问题,并说明现有插件功能与需求缺口,便于官方评估并实现“增量(append)模式”。本 Issue 仅聚焦 单架构增量发布 场景——双架构并行一次性发布目前运转正常,不在讨论范围。
1. 业务背景(Why)
说明 流水线规模 60 个仓库大多复用同一 CI 模板,部分还有每日定时构建。 架构矩阵 主流生产平台: linux/amd64与linux/arm64;常见开发节奏 90 % 的功能/缺陷修复只影响 AMD64;Arm64 需继续拉取稳定旧版镜像。 旧时代运行良好的策略 制品库曾使用 单列 manifest 格式,Shell 通过 docker manifest create --amend增量加入新架构条目,未触及其他架构。关键环境升级 2025‑07:制品库切换到 OCI multi‑index list;
Runner 升级 Docker 27 + containerd‑snapshotter(rootless)。旧 Shell 工作流程(示意)
# 1️⃣ 构建并推送本次变更的单架构镜像,例如 latest‑amd64 docker build -t $REG/foo/bar:latest-amd64 . docker push $REG/foo/bar:latest-amd64 # 2️⃣ 探测另一架构镜像是否已存在 if docker manifest inspect $REG/foo/bar:latest-arm64 >/dev/null 2>&1; then OTHER_PLATFORM_EXISTS=true fi # 3️⃣ 增量 or 单架构策略 if [ "$OTHER_PLATFORM_EXISTS" = true ]; then docker manifest create --amend $REG/foo/bar:latest \ $REG/foo/bar:latest-amd64 \ $REG/foo/bar:latest-arm64 docker manifest push --purge $REG/foo/bar:latest else docker tag $REG/foo/bar:latest-amd64 $REG/foo/bar:latest docker push $REG/foo/bar:latest fi运行结果: 线上
docker pull foo/bar:latest始终可拉到两架构镜像。
2. 插件现状 vs 需求(What)
维度 文档列明的插件能力 业务需求 差距 合并方式 传入 template + platforms→ 一次性docker manifest create && push增量合并:读取已存在 manifest,追加或替换当前架构条目,其余保持 ❌ 缺失增量流程 缺失镜像处理 ignoreMissing=true时不报错,但产出 list 只含现有镜像未构建架构必须沿用旧版本 ❌ 无法保留旧平台 细粒度注解 无 variant / digest参数需要能标注 --variant v8等❌ 功能缺失 CLI 依赖 使用 Docker CLI manifest子命令 (Experimental)需跟随官方路线迁移至 Buildx imagetools ⚠️ 未来可能失效 多 target 支持 可一次推送 latest, v1.0.0等多个 tag已满足 ✅
3. 复现步骤(Step by Step)
- 前置状态:仓库已存在
foo/bar:latest-arm64(上一版 Arm64 镜像)。- 本轮构建:仅在 AMD64 Runner 生成并推送
foo/bar:latest-amd64。- 调用官方插件
- stage: manifest image: cnbcool/manifest settings: target: foo/bar:latest template: foo/bar:latest-OS-ARCH platforms: - linux/amd64 # ⚠️ 本轮只列 amd64- 预期:
latesttag 继续同时包含 amd64 + arm64。- 实际:插件生成的新 manifest list 仅含 amd64;Arm 服务器执行
docker pull foo/bar:latest→manifest unknown(404)。
关键源码证据链(为什么只剩 amd64)
• 函数
GetManifests只遍历传入的platforms,为每个元素生成一条
ManifestEntry,没有任何逻辑去读取旧 manifest:
https://cnb.cool/cnb/plugins/cnbcool/manifest/-/blob/main/utils/args.go#L58-82• 生成完的
manifests列表随后被封装成YAMLInput并传给
registry.PushManifestList,该 API 直接覆盖目标 tag 的旧 manifest,
不做增量合并:
https://cnb.cool/cnb/plugins/cnbcool/manifest/-/blob/main/utils/args.go#L31-46→ 结论:当
platforms只写 amd64 时,新 manifest list 只包含 amd64,
旧的 arm64 子镜像被覆盖,Arm 机器拉取失败。-------------------- 代码片段解读 --------------------
func GetYamlInputs(args *Args) ([]types.YAMLInput, error) {
// Modified from https://github.com/estesp/manifest-tool/…/push.go
inputs := []types.YAMLInput{}targets := strings.Split(args.Target, ",") // ❶ 允许同时处理多个 target for _, target := range targets { manifests, err := GetManifests(args.Platforms, args.Template) // ❷ if err != nil { return inputs, err } input := types.YAMLInput{ // ❸ 构造单个 YAMLInput Image: target, // *Image* 字段就是要推送的 tag Manifests: manifests, // ManifestEntry 列表 Tags: nil, // 没用到 tag alias 功能 } inputs = append(inputs, input) // ❹ 累加到返回切片 } return inputs, nil}
────────────────────────────────────────────────────────
解释关键点
targets 处理
args.Target可以是逗号分隔的多个 tag;函数会为每个 tag 构造
一个独立的YAMLInput,最终一次推送到多个目标。
(示例 YAML 里的foo/bar:v1.0.0,foo/bar:latest就在这里拆分)GetManifests 调用
核心决定权 全在GetManifests(args.Platforms, args.Template):
• 仅根据 调用方传入的platforms生成 ManifestEntry 列表;
• 不读取远端旧 manifest,也没有追加逻辑。
因此如果platforms只包含linux/amd64,返回列表就只有 amd64。YAMLInput 结构
manifest-tool 会把YAMLInput里的Manifests直接写进新的
manifest list,然后推送到Image指定的 tag。旧 manifest
条目(例如 arm64)不会被合并进来,而是被覆盖掉。与复现步骤对应
当你在 settings 里只写platforms: [linux/amd64]:
•GetManifests→ 只生成 amd64 条目
•registry.PushManifestList→ 将仅含 amd64 的 list 覆盖到
foo/bar:latest
• Arm64 节点 pulllatest⇒ 404.结论:这段代码正是“只要 platforms 缺少某平台,就会丢失该平台”
的直接根源,也解释了复现步骤中的实际表现。
4. 主要风险点(Risk)
编号 风险点(与历史 Shell 逻辑直接冲突) 触发条件 结果 A Shell 正则依赖 config/layers字段来判断“是否单架构 manifest”<tag-arch>被误推成 OCI indexShell 误判“镜像不存在”,走单架构直推流程,导致线上另一平台镜像被删除
5. 期望功能(Expectations)
mode: append — 当目标 tag 已存在时,插件应先
manifest inspect旧 list,再追加 / 覆盖当前架构条目并推送,
未列架构保持不变。
↳ 官方等价 CLI:docker buildx imagetools create --append …
↳ 证据链:Docker 文档 “Append new sources to an existing manifest list”
https://docs.docker.com/reference/cli/docker/buildx/imagetools/create/#append-new-sources-to-an-existing-manifest-list---appendvariantMap — 允许在 settings 中为各平台附加
variant、显式
digest等元数据,以满足 arm64/v8 等精细场景。
↳ 官方等价 CLI:docker manifest annotate … --variant v8 …
↳ 证据链:Docker 文档docker manifest annotate支持 --variant
https://docs.docker.com/reference/cli/docker/manifest/annotate/
↳ 插件目前 无此字段:README 仅列出username / password / target / template / platforms / skipVerify / ignoreMissing
https://cnb.cool/cnb/plugins/market/-/blob/b7329821a3a2debd403ac51015c3772764d293e5/plugins/cnbcool/manifest/README.md只要实现
mode: append,我就能删除复杂 Shell,让所有这些仓库通过一行 YAML 安全完成单架构迭代,并随时扩展更多架构。
号外: docker buildx 异构打包功能开放公测啦!
目前通过流水线中指定
runner.tags:cnb:arch:amd64:containerd-snapshotter,可以将构建指定到支持containerd-snapshotter特性的机器(ps. 未来这个标签会去掉,仅供测试,请勿在正式环境中使用)具体使用方法可参考: https://cnb.cool/loviselu/docker-buildx-multi-platform-example
未来计划:等整个流程跑通没啥问题,后续会把平台所有机器都开启这个特性,无需大家单独指定tag
[Allocating Runner] Pipeline prepare error: No runner for namespace: global tags: cnb:arch:amd64:containerd-snapshotter, cpus: -1. Reference document: https://docs.cnb.cool/build/build-node.html
正式环境没有开启这个特征,却把测试标签 cnb:arch:amd64:containerd-snapshotter 删掉了?
还想着先用测试标签过度一下,等正式环境支持后,再把测试标签去掉。@loviselu(卢嘉辉)
这个主要是 Docker 默认的 builder 实例使用的 driver 是 docker,不支持多平台和缓存功能(参见 Build drivers | Docker Docs),错误信息示例如下:
[docker build and push] ERROR: Multi-platform build is not supported for the docker driver.
[docker build and push] Switch to a different driver, or turn on the containerd image store, and try again.
[docker build and push] Learn more at [https://docs.docker.com/go/build-multi-platform/](https://docs.docker.com/go/build-multi-platform/)
然后使用 docker buildx create --use --bootstrap --name mybuilder --driver=docker-container 创建 builder 也会报错,错误信息示例如下:
ERROR: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error reopening /dev/null inside container: open /dev/null: operation not permitted: unknown
这个我猜测应该是由于 docker 服务是以普通权限模式运行在 k8s 上 dind 容器,而 docker buildx create 会使用最高权限创建 builder,所以产生的权限问题。
我测试的临时解决方案就是单独运行一个 rootless 的 buildkitd 容器,然后再创建一个 driver 为 remote 的 builder 作为当前默认 builder,命令如下所示:
docker run -d --name buildkitd \
--security-opt seccomp=unconfined \
--security-opt apparmor=unconfined \
--security-opt systempaths=unconfined \
moby/buildkit:rootless &\
docker buildx create --use \
--name mybuilder \
--driver remote docker-container://buildkitd
将上述命令作为一个阶段在 docker buildx build 之前执行就行,但是好像 buildkitd 容器完全启动需要一定时间,如果马上执行构建可能会出现超时情况,可以考虑中间执行其他任务或者 sleep 几秒。
示例用法可参考:hzboiler/docker-buildx-multi-platform-example · Cloud Native Build
![]()
[Allocating Runner] Pipeline prepare error: No runner for namespace: global tags: cnb:arch:amd64:containerd-snapshotter, cpus: -1. Reference document: https://docs.cnb.cool/build/build-node.html正式环境没有开启这个特征,却把测试标签
cnb:arch:amd64:containerd-snapshotter删掉了?还想着先用测试标签过度一下,等正式环境支持后,再把测试标签去掉。@loviselu(卢嘉辉)
@ujcms(PONY) 开始这个特征后,机器每隔一段时间就会出故障,所以先都去掉了。等后续彻底解决又再重新放开
https://cnb.cool/examples/ecosystem/docker-buildx-multi-platform-example
可以试一下这个方案,任意类型的机器都可以使用。
https://cnb.cool/examples/ecosystem/docker-buildx-multi-platform-example
可以试一下这个方案,任意类型的机器都可以使用。
@hekangning(宁宁) 我去测试一下
@hekangning(宁宁) 提示:
runc run failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount src=proc, dst=/proc, dstFd=/proc/thread-self/fd/8, flags=MS_NOSUID|MS_NODEV|MS_NOEXEC: operation not permitted
@hekangning(宁宁) 提示:
runc run failed: unable to start container process: error during container init: error mounting "proc" to rootfs at "/proc": mount src=proc, dst=/proc, dstFd=/proc/thread-self/fd/8, flags=MS_NOSUID|MS_NODEV|MS_NOEXEC: operation not permitted
@zishuo(山里人)
修复代码已经提交,等发版
这个特性将解决什么问题?
我们目前在 Github 依赖 GitHubCI 的能力,在对异构的Docker镜像进行打包,例如 LoongArch64。
有计划将 Github 的 CI 迁移到CNB,但是暂时还不支持打包异构镜像。
设想的解决方案?如有
官方已有解决方案