Docker Buildx多架构镜像迁移:原理、实操与避坑指南
最近在做镜像仓库搬迁的时候遇到一个绕不开的坎儿仓库里存着大量多架构镜像尤其是linux/amd64和linux/arm64并存的情况。直接用docker pulldocker tagdocker push这种老办法很容易把 manifest list 拆散进到新仓库里只剩一个平台arm 节点一拉就报no matching manifest。后来把docker buildx用起来整套流程才算真正跑顺。这篇文章就围绕“docker buildx 做多架构镜像仓库迁移”这件事把原理、准备、实操命令、排查思路和批量脚本一次讲清楚。适合正在做镜像同步、机房搬迁、云厂商切换、自建 Harbor 迁移的运维和开发同学参考也适合那些想把手头 x86 镜像补出 arm64 版本的人。1. 为什么多架构镜像迁移不能只靠 docker pull/push1.1 多架构镜像的本质是 manifest list在深入命令之前得先弄明白多架构镜像到底长什么样。普通单架构镜像只有一个 manifest指向一个 config 和若干 layer。多架构镜像则多了一层“索引”也就是 manifest listOCI 里叫 index。这层索引里记录着每个平台对应的 manifest digest比如linux/amd64指向sha256:aaa...linux/arm64指向sha256:bbb...Docker 在拉取时会根据当前节点的 CPU 架构自动选择对应的 manifest然后在本地展开 layer。这也是为什么同一个 tag 能在 x86 和 arm 机器上都能跑因为 tag 背后的 manifest list 本身就是“一个入口多份实体”。理解这一点迁移的核心任务就很清楚了不只是把 blob 和镜像配置搬过去更要把这层索引关系完整搬过去。docker pull默认只拉当前平台的 manifest并不会把整个 manifest list 原样取回本地所以再用docker tag推到新仓库时丢掉的恰恰就是其他平台的指针。1.2 buildx 在这个场景里的真正角色docker buildx是 Docker 官方的多架构构建插件底层走 BuildKit。很多人对它的印象停留在“一条命令同时出 amd64 和 arm64 镜像”这没错但它还有另一个不太被注意的能力docker buildx imagetools子命令可以直接操作 registry 上的 manifest。这意味着构建新镜像可以用docker buildx build --platform ... --push而迁移存量镜像则可以用docker buildx imagetools create把多个平台的 manifest 重新组合并推到新仓库。两条路径都在 buildx 的能力范围之内这也是我最终确定用 buildx 做迁移主线的核心原因。注意如果你的源仓库本身就是标准的 manifest list最省事的方式其实是用skopeo copy或crane cp它们能整段复制索引。buildx 的强项在于“重新组合”和“跨仓库编排”尤其在源仓库里各架构镜像是用不同 tag 存放比如app-amd64、app-arm64的时候buildx 几乎是命令级最优解。2. 迁移前必须摸清的三件事2.1 盘点源仓库的镜像清单与架构分布迁移前最忌讳直接上手跑脚本先要把家底盘清楚。我习惯分三步走用 registry API 或skopeo list-tags列出所有需要迁移的仓库和 tag。对每个 tag 检查是否已经是多架构索引可以用docker buildx imagetools inspect或skopeo inspect --raw查看。区分单架构镜像和多架构镜像分别走不同迁移策略。这里有个容易漏的点很多仓库里同一应用有好几个 tag比如1.0.0、latest、dev但它们指向的 digest 可能完全一样。迁移时先按 digest 去重能省掉一大半重复推送。命令方面我会用docker buildx imagetools inspect --format {{.Manifest.Digest}}之类的方式快速提取 digest再排序去重后生成任务清单。# 查看某个远端镜像的详细索引信息 docker buildx imagetools inspect registry.example.com/demo/app:1.0.0 # 只看平台列表 docker buildx imagetools inspect --raw registry.example.com/demo/app:1.0.02.2 准备跨架构构建与推送环境buildx 要用到docker-containerdriver因为默认的dockerdriver 不支持多平台镜像构建。创建自定义 builder 时需要指定 platform 参数让 BuildKit 知道要准备的运行环境种类。# 创建支持多架构的 builder 实例 docker buildx create \ --name multiarch-builder \ --driver docker-container \ --platform linux/amd64,linux/arm64 \ --bootstrap # 切换到该 builder docker buildx use multiarch-builder创建过程中BuildKit 会为每个平台准备对应的前端和运行环境第一次执行时可能因为要拉取相关镜像而稍慢属正常现象。迁移大量仓库时这个 builder 实例可以长期保留它会缓存构建环境避免每次重新初始化。如果你有自建镜像仓库还要确认新仓库支持多架构清单的写入。绝大多数现代 registry 都支持 OCI Media Types但如果目标仓库版本过旧可能只支持 Docker Schema 2 而无法接收多平台索引。迁移前最好拿一个测试镜像走一遍全流程确认两端仓库兼容性再放开批量执行。2.3 认证配置与仓库地址梳理迁移涉及源仓库和目标仓库两边的凭证。我强烈建议用 docker CLI 的docker login先把两端都登录好因为 buildx 的 imagetools 命令默认会复用~/.docker/config.json里的认证信息。相比把密码写进脚本用凭证管理器或 CI 的 secret 要安全得多。# 登录源仓库 docker login registry-source.example.com # 登录目标仓库 docker login registry-destination.example.com如果你的源仓库是自建 Harbor 且开启了机器人账号要确认该账号对每个项目都有pull权限目标仓库账号则需要对应项目的push权限。迁移过程中最常见的认证报错就是denied: requested access to the resource is denied排查思路第一优先就是看当前 docker 配置里面对应的 registry 地址是否已登录、账号权限是否充足。3. 两种迁移场景的实操拆解3.1 场景一源仓库本来就是标准多架构索引这种情况最好办。源仓库的 tag 后端已经是一个 manifest list迁移目标就是把整个索引搬过去并保留平台信息。最直接的方式是用skopeo copy因为它会把镜像完整地从源 registry 复制到目标 registry保持 digest 不变。skopeo copy \ --src-creds user:pass \ --dest-creds user:pass \ docker://registry-source.example.com/demo/app:1.0.0 \ docker://registry-destination.example.com/demo/app:1.0.0但如果是想坚持“全程只用 buildx”也可以用docker buildx imagetools create来把各平台 manifest 组合到新仓库。做法是先查看源镜像的每个平台 digest然后在新仓库里手动构建一个 manifest list。第一步用 inspect 拿到每个平台的索引docker buildx imagetools inspect \ --raw registry-source.example.com/demo/app:1.0.0输出会包含manifests数组每个元素里有platform和digest。把 amd64 和 arm64 对应的 digest 摘出来然后执行docker buildx imagetools create \ --tag registry-destination.example.com/demo/app:1.0.0 \ registry-source.example.com/demo/appsha256:amd64平台的digest \ registry-source.example.com/demo/appsha256:arm64平台的digest这一步的实质就是新生成一个 manifest list 并推送到目标仓库。它不会把旧索引原样复制而是按你指定的 digest 重新组合。好处是可以借机过滤掉不再需要的平台比如你只想要 amd64 和 arm64顺手把linux/386丢掉。3.2 场景二源仓库里各架构是独立 tag这种场景在大型组织里非常常见。由于早期没有用 buildx 统一出镜像CI 流水线往往是分平台构建后分别推送形成app-1.0.0-amd64、app-1.0.0-arm64这种命名风格。迁移时要做的就是把这些独立 tag 合并成一个多架构 tag。docker buildx imagetools create \ --tag registry-destination.example.com/demo/app:1.0.0 \ registry-source.example.com/demo/app-1.0.0-amd64 \ registry-source.example.com/demo/app-1.0.0-arm64如果还有特殊平台比如linux/arm/v7继续在后面追加即可。执行完成后用 inspect 验证docker buildx imagetools inspect \ registry-destination.example.com/demo/app:1.0.0输出里能看到Platform: linux/amd64、Platform: linux/arm64两行说明组合成功。这里有一个值得注意的细节docker buildx imagetools create组合出来的新 tag 的 digest 和源镜像 digest 是不同的因为 manifest list 的 digest 取决于内部指针的排列方式。如果下游有依赖固定 digest 的部署系统这个差异要提前告知相关团队。3.3 场景三源仓库没有 arm64 镜像需要顺手补齐迁移过程中经常被顺带追加需求“既然都要迁到新仓库了不如把 arm64 顺便补出来。”这恰恰是 buildx 最典型的场景。如果你手头有 Dockerfile 和源码直接基于源码构建多架构镜像并推送一步到位。docker buildx build \ --platform linux/amd64,linux/arm64 \ --tag registry-destination.example.com/demo/app:1.0.0 \ --push \ .但有个前置条件基础镜像也必须支持多架构。比如alpine:3.20、debian:bookworm-slim这些官方镜像都支持如果你的基础镜像是内网制件且只有 amd64arm64 构建会在「拉取基础镜像」阶段就失败。这时候你得先给基础镜像本身做一次架构补齐再回来构建应用镜像。无法重建的应用可以退而求其次检查是否有对应的 arm64 版本 tag。如果历史原因导致 arm64 版本早就丢失那这个应用在新仓库里只能保留 amd64 单架构并把情况记录在迁移清单中不要为了强行多架构而伪造 manifest那会让集群节点在调度时误判。4. 迁移过程常见问题与排查实录4.1 问题一docker driver 不支持多平台构建错误信息通常是ERROR: multiple platforms feature is currently not supported for docker driver原因默认的dockerdriver 依赖 Docker daemon 本身无法模拟多平台构建环境。解决办法是切换到docker-containerdriver即执行前文中的docker buildx create --driver docker-container。在这个过程中Builder 实例里的 BuildKit 会为每个平台创建独立的执行环境所以支持交叉构建比如在 x86 机器上产出 arm64 镜像。4.2 问题二目标仓库没有正确写入 manifest list迁移后在 amd64 机器上正常到 arm64 机器上执行docker pull报Error response from daemon: manifest for registry-destination.example.com/demo/app:1.0.0 not found: manifest unknown: manifest unknown这个报错基本可以断定目标仓库里只有一个单架构 manifest而 不是 manifest list。最常见原因是使用了docker pulldocker tagdocker push这种方式。Docker 客户端默认只拉取当前平台的 manifest推送时自然只会推送那一个平台。解决方式用docker buildx imagetools create重新组合各平台的 manifest或者直接用skopeo copy保留原始索引。如果已经在错误的方式下推了多次先在目标仓库删除该 tag再重新推送正确版本避免留下“半损坏”的索引缓存。4.3 问题三arm64 镜像拉取失败但 inspect 看起来正常命令docker buildx imagetools inspect显示有 amd64 和 arm64但实际从 arm 节点拉取却失败错误是no matching manifest for linux/arm64 in the manifest list entries排查思路确认索引里的 arm64 条目指向的 digest 是否真实存在于目标仓库。很有可能是组合时写错了 digest或者源仓库中那个 arm64 镜像已经变成了孤儿 blob。可以用以下命令检查目标仓库里对应 digest 是否存在docker buildx imagetools inspect \ registry-destination.example.com/demo/appsha256:那个arm64digest如果这个 digest 不存在说明当时imagetools create只是写入了指针实际 blob 没有跟随过去部分实现会依赖源仓库 blob 可访问性如果源仓库后续被清理就会出问题。稳妥做法是先把各架构镜像显式推送到目标仓库再做索引组合。4.4 问题四批量迁移时频繁被限流或中断大量镜像并发推送很容易触发目标仓库的限流典型错误是toomanyrequests: You have exceeded the pull rate limit on the image我的建议是不要无脑全并发控制在 3 到 5 个并发任务比较稳。每个镜像推送完成后等待一个随机间隔1 到 3 秒能明显降低被限流的概率。同时做好失败重试机制一般连续失败三次就跳过并记录等第一轮结束后单独重跑比死磕单个镜像更高效。另一个技巧是对于大镜像尽量在本地先做一次docker pull确认 blob 完整再走skopeo copy --all或docker buildx imagetools create。因为镜像迁移本质上是跨网络传输网络抖动容易导致传输中断本地先做缓存至少能排除“本地磁盘损坏”这类不常见但存在的低级问题。5. 批量迁移脚本设计与实操心得5.1 使用 buildx imagetools 的批量迁移脚本以下脚本是我在一次迁移中实际使用的简化版本。它读取一个“源镜像地址列表”每个地址对应一行然后对每一行提取平台 digest再组合推送到目标仓库。#!/usr/bin/env bash set -euo pipefail SOURCE_REGISTRYregistry-source.example.com DEST_REGISTRYregistry-destination.example.com IMAGE_LISTimages.txt # 示例 images.txt 每一行格式 # demo/app:1.0.0 # demo/middleware:2.1.0 while IFS read -r image; do if [ -z $image ]; then continue fi echo 处理 $image # 获取源镜像每个平台的 manifest digest raw_amd64$(docker buildx imagetools inspect --raw $SOURCE_REGISTRY/$image | \ jq -r .manifests[] | select(.platform.architecture amd64) | .digest | head -n1) raw_arm64$(docker buildx imagetools inspect --raw $SOURCE_REGISTRY/$image | \ jq -r .manifests[] | select(.platform.architecture arm64) | .digest | head -n1) echo amd64 digest: $raw_amd64 echo arm64 digest: $raw_arm64 # 如果两个 digest 都为空说明源镜像可能是单架构直接复制 if [ -z $raw_amd64 ] [ -z $raw_arm64 ]; then echo 未检测到多架构使用 skopeo copy skopeo copy \ docker://$SOURCE_REGISTRY/$image \ docker://$DEST_REGISTRY/$image \ --all continue fi # 构建参数 create_args(--tag $DEST_REGISTRY/$image) if [ -n $raw_amd64 ]; then create_args($SOURCE_REGISTRY/$image$raw_amd64) fi if [ -n $raw_arm64 ]; then create_args($SOURCE_REGISTRY/$image$raw_arm64) fi echo 执行 buildx imagetools create docker buildx imagetools create ${create_args[]} # 推送后验证 docker buildx imagetools inspect $DEST_REGISTRY/$image /dev/null done $IMAGE_LIST echo 全部镜像处理完成这个脚本的关键在于用jq解析--raw输出并抽取指定平台的 digest。如果你的源镜像平台更多可以扩展为遍历jq里的所有 platform。5.2 批量任务中的限流、重试与回滚策略单脚本跑到底一定会出问题原因很现实几千个 tag 下来总有几个会因为各种各样原因失败。我一般会做一个“三清单”机制success.txt成功完成的镜像列表failed.txt需要重试的镜像列表skipped.txt确认不迁移的镜像列表比如已经废弃的 tag脚本每处理完一个镜像就把结果写进对应文件。第二轮只处理failed.txt中的内容重试三次仍失败的就进入skipped.txt最后人工核查。这样比无脑重跑全量省时间得多。回滚优先级也要提前想清楚。如果新仓库出现问题最坏情况是回滚到源仓库。所以我在迁移期间不会删除源仓库的任何数据迁移完成并验证一周后才开始清理。验证的方式也很简单找一个能访问两个仓库的节点对新旧 tag 分别执行docker buildx imagetools inspect对比平台列表和 digest。5.3 关于 digest 保持与不可变 tag 的建议如果你们的发布系统大量使用了 digest 固定的部署方式镜像迁移最理想的情况是所有 blob 的 digest 保持一致。skopeo copy --all能做到这一点它是在 registry 之间直接复制不去改动任何 manifest 或 layer。docker buildx imagetools create则不同它创建一个新的 manifest list最外层的 digest 必然变化。如果你的下游不关心最外层 digest 而是直接使用内部各平台 digest那影响不大如果下游把整个 tag 的 digest 写进部署配置这部分需要在迁移完成后更新。我的经验是在迁移窗口内做一次全量比对遍历源仓库的每个 tag与目标仓库同名 tag 的Manifest.Digest做比较输出差异清单。用skopeo inspect --format {{.Digest}}或docker buildx imagetools inspect --format {{.Manifest.Digest}}均可。6. 从构建到迁移的统一工作流6.1 新建镜像直接用 buildx 完成“构建即迁移”如果你们公司还在用传统的分平台构建、再合并的上传流程我建议直接改成 buildx 一步到位。新的工作流可以简化成在 CI 里统一使用docker buildx build --platform linux/amd64,linux/arm64 --push推送镜像。分支环境使用独立的 tag比如dev-${commit}。生产发布时把dev-${commit}重定向到latest用docker buildx imagetools create --tag处理。这样做的好处是仓库里永远只有多架构镜像不存在“单一架构 tag 之后再合并”的补丁式流程。6.2 存量镜像迁移和新镜像构建的衔接存量镜像迁移完后团队很快就会面临下一个问题以后的新版本怎么出此时不要急着废弃旧迁移脚本建议把它改造成一个“同步工具”放在 CI 里定期执行用来保证源仓库可能是旧仓库和目标仓库之间的最终一致性。尤其是当两个仓库之间需要灰度切换、双写并行时一个可靠的同步任务比什么口头约定都强。我在实际项目中就是把迁移脚本演进成同步脚本的每天深夜跑一次自动把源仓库新增的 tag 同步到目标仓库并且校验平台完整性。等运行两个月没有问题后才彻底把源仓库的读流量切换到新仓库。6.3 常见坑位清单快速回顾一下最容易踩的几个坑用docker tag迁移多架构镜像最终导致平台丢失忘记切换 buildx driver直接在默认 builder 上执行多平台构建基础镜像本身不是多架构导致 arm64 构建失败组合 manifest list 时写错 digest导致 arm 节点拉取失败批量迁移时并发过高触发仓库限流忽略了目标仓库对 OCI Media Types 的兼容性迁移完成后立刻清理源仓库回滚无路这些坑我基本都亲身趟过每次修复的时间成本都远高于提前做验证。尤其是“误以为迁移成功”往往是最危险的因为所有命令都返回成功但真实用户一拉就炸。7. 个人经验补充一些命令之外的细节最后补充几个命令行之外的经验。我自己的习惯是迁移前先在目标仓库手动创建一个测试项目迁一个无关紧要的小镜像比如几十 MB 的 busybox走完整链路确认目标仓库的 API 版本和 manifest 支持都正常。别小看这一步它能在批量执行前就暴露绝大多数兼容性问题。另外docker buildx imagetools create这个命令很多人只在多架构构建时才想起实际上它在“重新整理 manifest”方面非常灵活。除了合并独立 tag它还可以做 tag 重命名、平台过滤、跨仓库复制甚至两个不同源仓库的镜像合并到一个目标 tag。这意味着即使你的镜像不是单仓库内部分平台 tag而是散落在不同 registry 上buildx 也能把它们组合成一个多架构 tag。如果你在迁移中发现某个镜像无论如何都拉不全还可以试一下docker buildx imagetools inspect --raw加上jq层层剖析把每个层级的 digest、媒体类型、平台信息都打出来往往能很快定位到具体是哪个指针失效。这个思路在处理大型基础镜像时尤其好用。我做这个迁移项目最大的体会是多架构镜像迁移表面上是“搬运数据”实际上是在搬运“结构和关系”。只有把 manifest list 这一层理解透了才不会被各个工具的输出结果带偏。buildx 在这里既解决构建问题又解决索引重组问题能省下很多“手工补丁式操作”。如果你也正在计划类似迁移建议先拿一个不影响业务的小仓库试验跑通后记下全过程再逐步扩大范围会比一上来就压全量稳妥得多。