很多人第一次真正上手 buildx 多平台镜像,都是在同一个尴尬场景里被逼出来的:手头只有一台 x86 的服务器或者开发机,但交付对象是 Apple Silicon 的 Mac、是 ARM 云主机、是树莓派,甚至是龙芯或者飞腾的机器。以前遇到这种需求,常规做法是再搞一台对应架构的机器远程构建,或者干脆让用户自己在目标机器上装环境,折腾得双方都想骂人。
我自己在这个问题上踩了整整一个下午的坑之后,才有底气说一句:只要把 Docker buildx 和 QEMU 的用户态模拟配合好,一台 x86 机器就能同时产出 amd64、arm64、arm/v7 等多套架构的镜像,整个过程不用换机器、不用交叉编译环境,一条 docker buildx build 命令搞定。这篇文章把从原理到实操、从顺利到翻车的完整脉络都梳理一遍,适合三种人看:一是刚接触多平台镜像、不知道从哪下手的;二是已经照着网上教程跑通过、但不知道每一步在干什么的;三是跑得多平台构建但被各种玄学报错折磨过、想把原理彻底搞明白的。
1. 为什么非要多平台镜像?一个 tag 背后的架构暗战
1.1 Docker Hub 上悄悄发生的架构分流
先讲一个你可能早就注意到、但没细想过的现象:同一个镜像标签,比如nginx:latest,在 x86 服务器上docker pull下来是 amd64 平台的,在树莓派上docker pull下来却是 arm64 或者 arm/v7 平台的。两个平台执行的代码完全不一样,但你用的标签一模一样,甚至镜像 ID 显示也不同。
这不是 Docker Hub 在“随机发药”,而是镜像仓库里存的根本不是单一镜像文件,而是一个manifest list(清单列表),在 OCI 规范里叫 image index。这个清单里记录了同一套应用在各平台下的实际镜像摘要信息,包括镜像层、架构、操作系统、变体等。当用户执行docker pull时,Docker 客户端会读取自己宿主机的GOARCH和GOOS,去清单列表里匹配对应平台的 manifest,然后只拉取这一份镜像层。
这套机制很像视频网站根据你的设备和网络自动推流:同一个网址,手机上看是 720P,电视上看是 4K,后端根据终端信息做分流。多平台镜像就是这个思路,只是分流依据从分辨率变成了 CPU 架构。
1.2 没有多平台镜像时,队伍根本带不动
如果镜像不带多平台清单,通常的做法是靠 tag 区分架构,比如nginx:amd64、nginx:arm64、nginx:armv7。看起来能解决问题,但实际用起来非常痛:
- 用户记不住自己该用哪个 tag,arm64 的机器如果手滑拉了 amd64 的镜像,跑起来直接
exec format error,还查不出原因。 - Dockerfile 里的
FROM写起来没法做架构感知,编排系统里也没法跨架构混部。 - 一旦 Kubernetes 集群是混合架构节点,同一个 Deployment 根本没法用同一个镜像 tag 完成调度,必须维护多套 YAML。
所以多平台镜像是现代镜像分发的标准姿势,不是炫技。理解了这层机制,后面所有 buildx 的配置、QEMU 的注册、builder 的创建,都是在为生成这份 manifest list 服务。
2. 拆解 QEMU 和 buildx 的分工:谁在模拟,谁在编排
2.1 binfmt_misc:Linux 内核的“自动翻译官”
先解决一个最常见的认知误区:多平台构建不是 buildx 在模拟 ARM,buildx 只负责编排,真正让 ARM 指令跑在 x86 上的是 QEMU。这两者的协作关系,搞明白了,之后排查错误能省一半时间。
Linux 内核识别一个可执行文件时,靠的是文件头部的魔数(magic bytes)。ELF 格式的文件头里写明了这个二进制是针对哪种 CPU 架构编译的。默认情况下,内核发现可执行文件架构不匹配,就直接拒绝运行,报exec format error。
但 Linux 提供了一个名为binfmt_misc的内核模块,允许你注册自定义的“解释器规则”。规则的内容大致是:当内核遇到某个 magic 特征的可执行文件时,不要直接拒绝,而是把它交给指定程序去处理。QEMU 用户态模拟器就是利用这个机制注册进去的。注册之后,你在 x86 的宿主机上执行一个 aarch64 的 ELF,内核会识别出这是 aarch64 格式,然后自动把它交给qemu-aarch64来翻译执行。
这里必须强调一下,构建镜像时用到的 QEMU 是用户态模拟,也就是qemu-<arch>这类程序,不是qemu-system-*那种整机模拟。用户态模拟不虚拟化硬件,只翻译目标架构的 CPU 指令和系统调用,开销比整机模拟小很多。这也是为什么 buildx 模拟构建虽然慢,但还不至于慢到完全不能用。
2.2 为什么必须要新建一个 builder?docker driver 和 docker-container driver 的差别
很多人第一次跑 buildx 时会困惑:明明 Docker 自带 buildx,为什么我直接docker buildx build --platform linux/arm64会报错?问题出在默认的 builder 上。
buildx 本质上是一个客户端插件,它把构建请求转发给后端的 BuildKit 实例执行。builder 可以有不同的驱动类型:
docker驱动:复用 Docker daemon 自带的旧式构建能力,不需要额外启动容器,但不支持多平台跨架构构建。你传--platform参数它能解析,但真正执行 RUN 指令时,它没有跨架构执行的能力,所以要么报错,要么只构建当前架构。docker-container驱动:buildx 会启动一个独立的 BuildKit 容器,这个容器能针对每个--platform分别创建独立的构建环境,结合 QEMU 就能做到同一个 Dockerfile 同时构建多个架构的镜像。
所以新建一个docker-container驱动类型的 builder,是走多平台构建的前提。这是由 driver 的能力边界决定的,不是你环境哪里坏了。
2.3 QEMU 到底参与了构建的哪些环节
再深一层:QEMU 的模拟不是整个构建过程全程参与的。构建镜像时,BuildKit 会拉基础镜像、解析 Dockerfile、执行每条指令:
- 拉取 base 镜像:不需要模拟,纯网络和存储操作。
COPY、ADD文件:不需要模拟,文件拷贝跟架构无关。RUN指令:这一步要执行目标架构的二进制,比如apk add、go build、npm install,此时内核通过 binfmt_misc 把命令转给 QEMU 执行。多平台构建慢就慢在这一步。
如果你的 Dockerfile 里没有RUN指令,只是把编译好的二进制用COPY塞进基础镜像,那么其实不太需要 QEMU 参与,这也是为什么很多静态编译语言(Go、Rust)的多平台构建相对顺畅的原因之一。
3. 实操全流程:从一台 x86 机器上产出 amd64 / arm64 / arm/v7 镜像
3.1 环境检查与 buildx 可用性确认
实际操作前,先确认环境。buildx 从 Docker 19.03 开始作为实验特性引入,Docker Desktop 和主流 Linux 发行版自带的 Docker 在较新版本中已经默认集成。先跑几条命令:
docker version docker buildx version docker buildx ls我的环境是 Docker 27.x,buildx 输出类似下面这样:
docker buildx version github.com/docker/buildx v0.17.1 0124a65ddocker buildx ls会显示当前默认 builder 是default,Driver 是docker,Platforms 里只有本机架构。
如果你的 Docker 版本太老,没有 buildx 命令,有两个处理方式:
- 升级 Docker 到较新版本,这是最省事的。
- 手动安装 buildx 插件,把 buildx 二进制放到
~/.docker/cli-plugins/docker-buildx并加执行权限。
老版本 Docker 如果要启用实验特性,还需要在~/.docker/config.json里设置"experimental": "enabled"。新版一般不需要。
3.2 注册 QEMU 用户态模拟器:一条命令背后发生了什么
这是整个流程里最容易出问题的环节,也是网上教程最含糊的地方。常见的命令是这一条:
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes我来逐段拆解这条命令在干什么:
docker run --rm:运行一个一次性容器,用完就删。--privileged:需要特权模式,因为容器里要写宿主机内核的/proc/sys/fs/binfmt_misc/register文件,特权模式才能访问宿主机的内核接口。multiarch/qemu-user-static:这个镜像内置了针对多种架构的 QEMU 用户态模拟器二进制,比如qemu-aarch64、qemu-arm、qemu-riscv64等。--reset:清除已存在的注册记录,重新注册,避免残留的旧规则干扰。-p yes:持久化注册。它会注册带F标志(fix-binary)的处理规则,让解释器路径被直接内联进 binfmt 规则,避免某些容器挂载文件系统下找不到解释器的问题。
执行完可以验证注册是否成功:
ls /proc/sys/fs/binfmt_misc/ cat /proc/sys/fs/binfmt_misc/qemu-aarch64如果qemu-aarch64文件存在,并且内容里有interpreter /usr/bin/qemu-aarch64-static之类的内容,说明注册成功。
这里有个很重要的补充:Docker Desktop for Mac / Windows 在较新版本(4.x 后)默认已经内置了 QEMU 注册机制,你在桌面上用 buildx 多平台构建,即使不手动执行上面这条命令,通常也能跑通。但 Linux 服务器,尤其是云服务器,基本都需要手动注册。而且WSL2 环境的同学要格外注意:WSL2 重启后,binfmt_misc 的注册可能会丢失,构建时如果突然报一堆诡异错误,优先重新跑一次这条注册命令。
3.3 创建并切换 builder
注册好 QEMU 之后,新建一个docker-container类型 builder:
docker buildx create --name multiarch --driver docker-container --use参数说明:
--name multiarch:builder 名称,随意。--driver docker-container:指定驱动类型,这是支持多平台的关键。--use:创建后立即切换为当前使用的 builder。
接着可以加一个--bootstrap参数让 BuildKit 容器立刻启动,或者用docker buildx inspect --bootstrap multiarch手动启动。执行docker buildx ls就能看到当前 builder 的 Platform 列表里多出了linux/amd64、linux/arm64、linux/arm/v7等模拟平台条目。
一句经验之谈:创建 builder 时不需要手动指定 QEMU 注册地址之类的配置,它只是启动一个 BuildKit 容器,容器内执行命令时由宿主机的 binfmt_misc 机制自动引导到 QEMU。
3.4 写一个对多平台友好的 Dockerfile:TARGETARCH 的作用
很多第一次跑多平台构建的人,栽在 Dockerfile 写得“太死了”。比如有些 Dockerfile 里写死apt-get install -y libc6-dev-amd64,或者干脆用uname -m判断架构再下载二进制,这些在单架构下没问题,多平台时很容易翻车。
利用好 BuildKit 自动注入的内置构建参数,是多平台 Dockerfile 的标准姿势。以 Go 项目为例:
FROM golang:1.22-alpine AS builder ARG TARGETOS TARGETARCH ENV GOOS=$TARGETOS GOARCH=$TARGETARCH WORKDIR /app COPY . . RUN go build -o myapp . FROM alpine:3.20 COPY --from=builder /app/myapp /usr/local/bin/myapp ENTRYPOINT ["myapp"]注意其中的TARGETOS和TARGETARCH:这是 BuildKit 根据命令行传入的--platform参数自动注入的。构建linux/arm64时TARGETOS=linux、TARGETARCH=arm64,Go 的编译器就会直接交叉编译出 arm64 的可执行文件,这个步骤本身不需要 QEMU 执行 arm64 指令,是在原生架构下完成交叉编译的。
如果需要模拟验证,可以写一个简单 Dockerfile:
FROM alpine:3.20 RUN apk add --no-cache curl && uname -m构建时uname -m会输出 QEMU 模拟下的架构名,比如aarch64,这就证明模拟链路是通的。
3.5 一条命令构建并推送多平台镜像
一切就绪后,构建并推送:
docker buildx build \ --platform linux/amd64,linux/arm64,linux/arm/v7 \ -t yourname/myapp:latest \ --push .拆解:
--platform指定目标平台列表,用逗号分隔。这里构建的是 x86_64、64 位 ARM、32 位 ARM v7 三个平台。-t指定镜像标签,注意是多平台共享同一个 tag。--push直接把构建结果和 manifest list 推送到镜像仓库,需要先docker login。.是构建上下文路径。
如果这里用了--load而不是--push,就会出现限制:--load只能把镜像加载到本地 Docker 镜像存储里,而传统 Docker 镜像存储不支持原生多平台 manifest list,所以--load在多平台构建时通常只能加载其中一个平台。想本地验证多平台镜像,要么用支持 containerd 镜像存储的 Docker Desktop 配置,要么老老实实推送到仓库再从仓库拉取验证。
3.6 验证镜像真的多平台了
推送完成后,验证 manifest list:
docker buildx imagetools inspect yourname/myapp:latest输出里会列出Platforms: linux/amd64, linux/arm64, linux/arm/v7之类的内容。或者用传统命令:
docker manifest inspect yourname/myapp:latest能看到不同平台对应的 manifest 摘要信息。
想实测运行效果,可以在一台 ARM 机器上docker pull后运行uname -m看输出;如果没有 ARM 机器,也可以在本地通过 QEMU 模拟跑起来:
docker run --rm --platform linux/arm64 yourname/myapp:latest uname -m前提是本地 Docker 有办法拉取并运行 arm64 镜像(macOS 的 Docker Desktop 借助 QEMU 直接支持,Linux 需要注册好 binfmt_misc),运行后输出aarch64即验证成功。
4. 亲身踩过的坑:模拟构建不是装好就能一路绿灯
4.1 “exec format error” 的排查链路:QEMU 注册是否还在
这是多平台构建报错频率最高的一条,现象是构建中途某个 RUN 步骤直接失败,错误信息里有exec format error,或者在本地直接运行其他架构镜像时也这样。
完整的排查链路我是这么走的:
- 先确认 QEMU 注册还在不在:
ls /proc/sys/fs/binfmt_misc/,看有没有qemu-*文件。 - 如果没有,重新执行
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes。 - 如果注册还在但还是报错,检查是不是在 WSL2 里运行,WSL2 重启会导致挂载层面的 binfmt 失效,需要重新注册。
- 还有一种隐蔽情况:如果你是在容器内跑 Docker(Docker in Docker),注册指令必须要在运行 Docker daemon 的宿主机上执行,而不是在容器内。比如很多 CI Runner 本身就是容器,此时 QEMU 注册要放在 Runner 宿主机的 agent 初始化阶段。
4.2 基础镜像没有目标架构变体
有些小众镜像或者某些镜像的旧 tag 只发布了 amd64,没有发布 arm64 变体。你在构建linux/arm64时就会报no matching manifest for linux/arm64 in the manifest list entries。
这个问题的排查方式很简单,先用docker buildx imagetools inspect <镜像名:tag>查看基础镜像支持的平台列表,如果确实缺平台,那就换基础镜像,或者自己从上游源码构建目标架构版本。还有一个容易出现的情况是平台变体不匹配:比如你想构建linux/arm/v7,但基础镜像只提供了linux/arm64,两者并不通用。
4.3 QEMU 模拟下的性能问题:大型编译项目慢到怀疑人生
这是我个人感受最深的一点。QEMU 用户态模拟虽然比整机模拟快,但指令翻译的开销依然可观,尤其是编译型项目。我用同样的 Dockerfile 构建一个包含大量原生依赖的 Go 项目时,amd64 平台用了大概 4 分钟,arm64 平台第一次构建跑了接近 25 分钟。
如果你的项目是大型 C/C++、Rust,或者 Node.js 项目里有大量需要编译的原生模块,arm64 在 QEMU 模拟下的构建时间可能长到超过 CI 超时限制。应对思路我在前面已经埋了伏笔:尽量让构建过程变成交叉编译,而不是模拟编译。
比如 Go 项目,上面 Dockerfile 里的GOOS=$TARGETOS GOARCH=$TARGETARCH就能让编译器直接产出 arm64 的二进制,整个过程不需要执行 arm64 指令,速度基本和原生构建持平。Rust 也有类似机制,设置CARGO_BUILD_TARGET即可。只有那些必须在目标架构下运行安装脚本的依赖(比如某些需要现场编译 C 扩展的语言包管理器),才真正依赖 QEMU 模拟。
4.4 uname -m 陷阱:模拟容器里的 CPU 名字不可靠
有个容易让人误判的场景:你在 QEMU 模拟的 arm64 容器里执行uname -m,它返回的是aarch64,这让你误以为“我的二进制是 aarch64 的”,但这种模拟不代表你的运行环境就等同于真正的 ARM 机器。
更常见的坑是 Dockerfile 或构建脚本里写了类似这样的判断逻辑:
ARCH=$(uname -m) if [ "$ARCH" == "x86_64" ]; then DOWNLOAD_URL="https://example.com/amd64.tar.gz" else DOWNLOAD_URL="https://example.com/arm64.tar.gz" fi在 QEMU 模拟下,变量判断的结果是目标架构的,所以会去下载 arm64 版本,这本身没有错。但脚本里如果反过来依赖这个结果去做宿主机侧的操作,或者用了某些 QEMU 不完全支持的系统调用,就会出各种奇怪问题。我的建议是构建脚本里优先使用 BuildKit 注入的TARGETARCH/TARGETPLATFORM参数,不要依赖uname判架构,前者是构建平台语义,语义更清晰,可读性也更好。
4.5 --load 和 --push 的取舍,别想着“先在本地验证一遍再推送”
很多刚从传统docker build切到 buildx 的人,习惯先用docker build在本地构建并运行验证,没问题再推送。但多平台构建时,“本地验证”这件事的语义变了。
我之前吃过亏:docker buildx build --platform linux/arm64,linux/amd64 --load -t myapp:dev .,结果构建完成本地docker images里看到的只有一个平台,另一平台根本没进本地镜像存储。查阅资料后确认了根因:传统 Docker 引擎的镜像存储是单平台模型,manifest list 需要 containerd 镜像存储或者直接推到 registry 才能完整保留。
所以现在的操作习惯是:需要验证的镜像直接推送到仓库,然后docker buildx imagetools inspect查 manifest,再配合docker run --platform在模拟环境里做运行验证。本地调试阶段,建议要么单独--load一个目标平台验证,要么用支持 containerd 存储的 Docker Desktop / 新版引擎,提前解决这个单平台问题。
5. 实测性能与优化思路:不是所有项目都适合 QEMU
5.1 一张表看明白:哪些场景适合模拟,哪些适合交叉编译
多平台构建不是银弹,选对策略比硬跑更高效。我一般用下面这张表来做技术选型:
| 项目类型 | 是否适合 QEMU 模拟构建 | 推荐策略 |
|---|---|---|
| 纯 COPY 静态文件 / 解释型脚本无原生依赖 | 非常适合 | 直接 buildx 多平台,速度快,开销小 |
| Python / Node 项目,但依赖里有预编译二进制或需源码编译 | 看情况 | 能用跨平台 wheel / 预编译包的优先构建;否则只能模拟,注意超时 |
| Go / Rust 等静态编译语言 | 适合,且推荐 | 用 TARGETARCH 交叉编译,QEMU 只辅助最终镜像的少量 RUN |
| 大型 C/C++、直接编译工具链 | 不建议 | 优先在原生 ARM 机器 / ARM CI Runner 上构建,或用交叉编译工具链 |
| 需要现场执行目标架构安装脚本的复杂依赖 | 只能模拟 | 做好缓存,拆分构建阶段,降低被迫重复构建的频率 |
这张表的核心逻辑是:模拟的代价取决于 RUN 指令里真正跑目标架构二进制的时间占比。占比越高,越需要慎重。
5.2 分层缓存是救命稻草,别浪费了
QEMU 模拟慢,但如果能充分利用 BuildKit 的缓存机制,噩梦能减轻很多。多平台构建的缓存策略有两个关键点:
第一,不同平台之间的缓存不共享。arm64 的构建缓存和 amd64 的构建缓存是分开的,因为 RUN 指令在 QEMU 里执行的上下文不同,缓存 key 自然不同。所以不要幻想一次构建两个平台能省一半时间,实际上几乎相当于构建两次。
第二,务必配置远程缓存。每次都从零开始在 QEMU 下构建 arm64 是灾难,用--cache-from和--cache-to把缓存推到仓库,后续构建能省大量时间:
docker buildx build \ --platform linux/amd64,linux/arm64 \ -t yourname/myapp:latest \ --cache-from type=registry,ref=yourname/myapp:cache \ --cache-to type=registry,ref=yourname/myapp:cache,mode=max \ --push .这样第二次构建时,只有变更的层会被重新执行 RUN,其他层直接命中缓存。对于依赖安装这类高频不变的步骤,效果立竿见影。
5.3 多阶段构建与镜像瘦身:既少编译又少模拟
多阶段构建在普通 Dockerfile 里是好习惯,在多平台构建里更值得坚持。原因很简单:最终阶段如果只是 COPY 和运行,QEMU 的负担会非常小。
看这个例子:前面那个 Go 项目的 Dockerfile,最终镜像是一个干净的 alpine 加一个编译好的二进制,最终镜像的 RUN 指令几乎不存在,所以 arm64 和 amd64 的构建速度差距主要在上一个阶段的go build编译。而go build是用交叉编译完成的,不快才怪。
反过来,有些人的 Dockerfile 习惯在运行时阶段安装一堆调试工具、包管理器,这些都会触发 RUN,QEMU 在这一阶段的效率损失是最明显的。所以我的原则是:能少 RUN 就少 RUN,能 COPY 就 COPY,最终镜像保持干净。
5.4 BuildKit 并行与资源分配:别让模拟构建等死
QEMU 模拟本身吃 CPU,多平台同时构建更吃资源。BuildKit 默认会尽量并行处理多个平台的构建任务,但如果宿主机 CPU 核心数不够,并行反而可能导致每个平台都在“抢 CPU”,整体时间反而更长。
我自己的服务器是 8 核 16 线程,构建 3 个平台时会看到 3 个 RUN 任务并行推进,CPU 占用几乎拉满。如果遇到内存或 CPU 瓶颈,可以适当控制并行度,比如拆成两次命令分别构建部分平台,或者给构建容器限额。这一点容易被忽略,毕竟本地开发机器往往没有服务器那么充裕的资源配额。
6. 从本地到 CI:把多平台构建固化到流水线里
6.1 GitHub Actions 里最标准的做法
本地能跑通,CI 里也应该能稳定复现。GitHub Actions 对多平台 Docker 构建支持得非常好,官方维护的 action 把这套流程封装得很顺手。一个最小的 workflow 示例:
name: build-multiarch-image on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: ghcr.io/yourname/myapp:latest这里的关键是前两步:setup-qemu-action底层就是在 runner 上注册 QEMU 的 binfmt_misc,setup-buildx-action负责创建合适的 builder。有了这两步开发环境里的手动操作都不需要了,流水线自己搞定。
6.2 在 GitLab CI 或自建 Runner 上复用同一个思路
如果用 GitLab CI,原理一样,只是写法不同。关键点在于:Runner 的执行环境如果不是宿主机而是 Docker 容器,那么 QEMU 的注册动作必须在容器外完成,或者在 Runner 的初始化脚本里以特权模式执行。我的做法是给 CI 流程增加一个前置阶段,注册 QEMU:
build-multiarch: stage: build before_script: - docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - docker buildx create --name multiarch --driver docker-container --use script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY" - docker buildx build --platform linux/amd64,linux/arm64 -t "$CI_REGISTRY_IMAGE:latest" --push . tags: - docker注意 GitLab Runner 如果用的是 docker executor,执行docker buildx需要有 DinD(Docker in Docker)的 socket 或者预先在宿主机装好 docker CLI。这些属于 CI 基础设施层面的细节,等卡住了再逐个排查也来得及。
6.3 多项目复用 builder:buildx bake 解决配置维护问题
当项目里镜像多、平台多、tag 也多时,用一行 buildx build 带一大堆参数会变得难以维护。buildx bake 是更优雅的解决方案,它用 HCL 或 YAML 配置定义构建任务,类似 docker compose 的体验。
示例docker-bake.hcl:
group "default" { targets = ["app", "worker"] } target "app" { context = "." dockerfile = "Dockerfile" platforms = ["linux/amd64", "linux/arm64"] tags = ["yourname/myapp:latest"] } target "worker" { context = "./worker" dockerfile = "worker.Dockerfile" platforms = ["linux/amd64", "linux/arm64"] tags = ["yourname/worker:latest"] }执行docker buildx bake --push就能一次构建并推送所有目标。公用配置还可以抽取变量,比如 registry 地址、构建参数等,多平台镜像的项目交给 CI 后,这套方式比裸 buildx 命令好维护得多。
多平台镜像这一套玩下来,我自己最深的体会是:技术链路本身并不复杂,难的是理解不同组件之间的边界和责任。QEMU 负责让目标架构的指令跑起来,buildx 负责编排多平台构建流程,binfmt_misc 负责内核层的自动转发,Builder 驱动决定了你能走到哪一步。任何一个环节的理解不到位,遇到报错都会陷入“照着教程重试”的循环。
如果你刚跑通第一个多平台镜像,下一步可以试试给自己的常用项目补上linux/arm/v7甚至linux/riscv64等更多平台。构建时间会变长,但当你看到 manifest list 里列出一长串 Platforms 的时候,还是会觉得之前踩的坑都值了。