5362 字
27 分钟
多平台镜像构建实战:buildx 双架构
2026-11-15

上一篇 《Dockerfile 入门:把自己的应用打包成镜像》 讲了怎么把应用打包成镜像。但那篇里构建出来的镜像有一个隐藏前提:它只能在和你构建机器相同 CPU 架构的机器上跑。你在 x86 服务器上构建的镜像,拿到 ARM 机器上就是跑不起来的。这篇把这个坑填上:用 buildx 一次构建出 amd64+arm64 双架构镜像,推到仓库之后,任何架构的机器 docker pull 都能自动拿到自己能跑的那一份。

为什么需要多架构#

先看看现在的机器版图,你手里的设备很可能早就横跨两种架构了:

设备/环境CPU 架构平台标识
普通 x86 云服务器、台式机amd64(x86_64)linux/amd64
ARM 云主机(部分厂商的 ARM 实例)arm64(aarch64)linux/arm64
树莓派 4/5(64 位系统)arm64linux/arm64
Apple M 系列芯片的 Macarm64linux/arm64
老树莓派、32 位 ARM 设备armv7linux/arm/v7

镜像本质上是「文件系统+可执行文件」的打包,而可执行文件是按 CPU 架构编译的机器码。amd64 的二进制给 ARM 的 CPU,它一个指令都不认识。所以在 x86 上构建的镜像搬到树莓派上启动,容器会立刻退出,日志里就是那句经典报错:

exec /app/server: exec format error

或者:

standard_init_linux.go:228: exec user process caused: exec format error

这句话翻译过来就是「这个可执行文件的格式,当前 CPU 执行不了」。它不是你代码写错了,也不是 Docker 坏了,纯粹是架构对不上。

这个坑在实际使用里出现的频率比想象中高。举几个典型场景:你在公司或云上的 x86 服务器开发调试一切正常,镜像推上仓库,回家想在树莓派上把服务也跑一份,拉下来一启动就退出;或者你用的是 M 系列芯片的 Mac,同事在 x86 机器上构建的镜像发给你,你本地怎么都跑不起来;再或者云厂商的 ARM 实例性价比更高,你想把服务迁过去省钱,结果发现手里所有自建镜像都得重新构建一遍。还有一种更隐蔽的:Docker Desktop 在 M 系列 Mac 上对单架构 amd64 镜像会用模拟硬撑,能跑但慢得离谱,很多人误以为是服务本身性能差。

解决办法不是在每台机器上各构建一次再手动区分标签(那样版本管理全乱套,使用者还得先查自己是什么架构),而是构建多架构镜像:同一个标签底下同时挂 amd64 和 arm64 两份镜像,谁拉取就自动给谁对应的那份。本篇的主线就是把这件事完整走一遍:先讲清 manifest list 的原理,再装 QEMU、建 builder、一条命令构建推送,最后验证清单并在两种机器上实测。

原理:manifest list 是什么#

多架构镜像靠的是一个叫 manifest list(镜像清单列表,OCI 规范里叫 index)的东西。理解它,后面的命令就都顺理成章了。

平时你拉一个镜像,实际发生的是两步:

  1. 先向仓库要这个标签的 manifest(清单),清单里记录了镜像的配置和每一层文件;
  2. 按清单把层文件下载下来组装。

多架构镜像在这个基础上多了一层:标签指向的不再是单个 manifest,而是一个 manifest list,里面列着这个标签在每个平台下各自对应的 manifest:

myapp:1.0(标签)
└── manifest list
├── linux/amd64 → manifest A(amd64 版的层与配置)
└── linux/arm64 → manifest B(arm64 版的层与配置)

Docker 客户端拉取时,会先看这份 list,再根据自己所在机器的架构自动挑对应的一份下载。所以使用者完全无感:同一条 docker pull myapp:1.0,在 x86 服务器上拉到 amd64 版,在树莓派上拉到 arm64 版。这就是一次构建、一个标签、处处运行的底层原理。

官方镜像(Nginx、PostgreSQL、Redis 这些)全都是这么干的,所以你从来没操心过它们的架构问题。现在轮到给你自己的镜像也配上这个待遇。

准备工作:确认 buildx 与 QEMU#

确认 buildx 可用#

buildx 是 Docker 官方的增强构建工具,现在 Docker Desktop 和新版 Docker Engine 都自带。先确认:

Terminal window
docker buildx version

能输出版本号(类似 github.com/docker/buildx v0.x.x)就可以。没有的话按第一篇的方法把 Docker 升级到较新版本,老版本的 docker build 不具备多平台能力。

给构建机装上 QEMU 模拟#

这里有个关键问题:你的构建机是 amd64 的,它怎么构建出 arm64 的镜像?构建过程中要运行 ARM 架构的程序(比如在 Dockerfile 里执行 RUN 安装依赖),amd64 的 CPU 直接跑 ARM 二进制同样会报 exec format error。

解法是 QEMU 用户态模拟:在内核层面注册一层翻译,遇到 ARM 二进制就交给 QEMU 模拟执行。Docker 官方提供了一键安装镜像:

Terminal window
docker run --privileged --rm tonistiigi/binfmt --install all

这条命令通过 binfmt_misc 机制把各种架构的模拟器注册进内核,--install all 表示把支持的架构全装上。装完检查一下:

Terminal window
ls /proc/sys/fs/binfmt_misc/

能看到 qemu-aarch64 等文件就说明 ARM64 的模拟已经就位。这个注册在宿主机重启后会失效,重启了重新跑一遍上面的命令即可。

WARNING

QEMU 是模拟执行,速度比原生 CPU 慢一个数量级。编译型语言的大型项目在模拟下构建可能慢到几十分钟甚至超时,这一节先把流程跑通,性能问题在后面「QEMU 的性能坑」一节专门讲。

创建 builder:为什么必须换驱动#

buildx 的构建工作由「builder 实例」承担,每个 builder 有一个驱动(driver)。默认的 docker 驱动直接借用 Docker 守护进程的能力,日常单架构构建没问题,但它有个硬伤:不支持一次构建多平台并合并输出。所以要先创建一个 docker-container 驱动的 builder,它会专门起一个构建容器,能力完整得多:

Terminal window
docker buildx create --name multi --driver docker-container --use
  • --name multi:给这个 builder 起个名字,随便取;
  • --driver docker-container:用容器驱动,这是关键;
  • --use:创建完立刻切换为当前默认 builder。

创建后把它启动并检查状态:

Terminal window
docker buildx inspect --bootstrap

输出里重点看 Platforms 一行,应该列出 linux/amd64, linux/arm64, ... 一长串平台,说明这个 builder 已经具备多架构构建能力(靠的就是上一步装的 QEMU)。

两个驱动的区别一次说清:

对比项docker 驱动(默认)docker-container 驱动
构建执行者Docker 守护进程本体独立的构建容器
单架构构建支持支持
一次多平台构建不支持支持
构建结果直接 --load 进本地镜像库支持单平台可以,多平台不行(见下文)
缓存导出(registry/gha 等高级缓存)受限完整支持
使用前提无首次使用自动拉取构建镜像

顺手记一下常用管理命令:

Terminal window
docker buildx ls # 查看所有 builder 和当前选中项
docker buildx use default # 切回默认 builder
docker buildx rm multi # 删除不用的 builder

实战:一条命令构建双架构并推送#

准备沿用 Dockerfile 篇的例子,一个极简应用即可。为了让没看过前篇的读者也能直接动手,这里把示例 Dockerfile 再贴一次,就是一个启动后打印一句话的小程序,重点不在应用本身,而在构建流程:

FROM alpine:3.20
COPY app.sh /app.sh
RUN chmod +x /app.sh
CMD ["/app.sh"]

其中 app.sh 只有一行实质内容:

#!/bin/sh
echo "hello from $(uname -m)"

特意让它打印 uname -m 的结果,就是为了验证时能一眼看出当前跑的到底是哪个架构的镜像:在 amd64 机器上输出 x86_64,在 arm64 机器上输出 aarch64,装错了一目了然。假设 Dockerfile 已经就绪,当前目录就是构建上下文,镜像准备推到 Docker Hub 的 yourname/myapp,版本 1.0。还没登录仓库的话先执行一次 docker login,推送环节会用到。

一条命令完成双架构构建+推送:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t yourname/myapp:1.0 \
--push .

逐个参数拆开讲:

  • --platform linux/amd64,linux/arm64:目标平台列表,逗号分隔不要加空格。buildx 会为每个平台各构建一份镜像;
  • -t yourname/myapp:1.0:镜像名与标签,和普通构建一样;可以多次写 -t 同时打多个标签(比如再加一个 :latest);
  • --push:构建完成后直接把 manifest list 连同各架构镜像一起推到仓库;
  • .:构建上下文路径,和 docker build 一样是最后一个参数。

构建过程中你会看到进度里每个步骤都标注了平台(如 [linux/arm64 3/5] RUN ...),两个平台并行推进。注意观察的话会发现 arm64 那一路的 RUN 步骤明显更慢,那就是 QEMU 模拟在干活,属于正常现象。推送完成后,仓库里 1.0 这个标签就是一份双架构清单了。

如果想同时维护一个跟随最新版的 latest 标签,不用构建两遍,在同一条命令里多写一个 -t 即可,两个标签共用同一次构建结果:

Terminal window
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t yourname/myapp:1.0 \
-t yourname/myapp:latest \
--push .

还有一个容易忽略的细节:多平台构建时,每个平台的构建是互相独立的,Dockerfile 里如果有步骤只对某个平台成立(比如某个依赖只有 amd64 版),构建会在那个平台上单独失败,报错信息里会标明是哪个平台、哪一步挂的,看日志时认准平台前缀再对症处理,别被另一平台的正常输出干扰。

为什么多平台构建不能 —load#

单架构时我们习惯 --load 把镜像加载进本地 docker images 里先跑一下看看。多平台构建时这招用不了,原因是本地镜像库的存储结构装不下一份 manifest list——它只认单个架构的镜像。所以 buildx 对多平台构建的要求很明确:

  • 要么 --push 直接推到仓库(最常用);
  • 要么 --output type=oci 之类的方式导出成文件。

如果你硬给多平台构建加上 --load,会直接报错:

ERROR: docker exporter does not currently support exporting manifest lists
WARNING

记住这个分工:多平台构建的结果只能推仓库,不能进本地镜像库。想在本地验证,就从仓库把镜像按指定平台拉回来跑(下一节演示),这也是最真实的验证方式——模拟一遍使用者拿到镜像的全过程。

顺带一提,单平台时 --load 依然可用,比如只想本地快速试一下 arm64 版:

Terminal window
docker buildx build --platform linux/arm64 -t myapp:test --load .

这会在 amd64 机器上把 arm64 镜像加载进来,之后 docker run --platform linux/arm64 myapp:test 就能靠 QEMU 模拟跑起来做冒烟测试(慢,但能验逻辑)。

验证:清单要亲眼看到,两台机器都要试#

看 manifest list#

用 buildx 自带的 imagetools 检查刚推送的标签:

Terminal window
docker buildx imagetools inspect yourname/myapp:1.0

输出大致长这样,重点看 Manifests 下面是不是每个平台一份:

Name: docker.io/yourname/myapp:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:xxxx...
Manifests:
Name: docker.io/yourname/myapp:1.0@sha256:aaaa...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Name: docker.io/yourname/myapp:1.0@sha256:bbbb...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/arm64

顶层 MediaType 是 image.index(即 manifest list),底下挂着 amd64、arm64 两份 manifest,这就对了。如果只看到一份 manifest、顶层也不是 index,说明构建时平台参数没生效,回头检查命令。

在两种机器上分别拉取运行#

最后一步是真实环境验证。在一台 x86 服务器和一台 ARM 设备(树莓派、ARM 云主机都行)上分别执行:

Terminal window
docker pull yourname/myapp:1.0
docker run --rm yourname/myapp:1.0

两边用的是完全相同的命令、完全相同的标签,各自拉到自己架构的镜像并正常运行,这才算大功告成。想确认某台机器拉到的到底是哪一架构,可以看镜像详情里的架构字段:

Terminal window
docker image inspect yourname/myapp:1.0 --format '{{.Architecture}}'

在 x86 上输出 amd64,在 ARM 上输出 arm64,各就其位。再配合示例程序打印的 uname -m,双保险确认无误。

如果手边暂时只有一台机器,也可以在本机强制拉取并运行另一个平台的版本做验证(靠 QEMU 模拟运行,慢但能确认镜像本身没问题):

Terminal window
docker run --rm --platform linux/arm64 yourname/myapp:1.0

在 amd64 机器上执行这条命令,输出 hello from aarch64,就说明 arm64 那份镜像是真能跑的,而不是清单里挂了个空壳。发布重要版本前花一分钟做这个检查,能挡住绝大多数「清单对了但某个平台的镜像是坏的」事故。

Dockerfile 的架构适配注意#

多平台构建对 Dockerfile 有两个常见要求,提前知道能少踩坑。

基础镜像必须有多架构版本#

你的 FROM 用的基础镜像如果只有 amd64 版,那 arm64 的构建会在第一步就失败。好消息是主流官方镜像(python、node、golang、nginx、alpine 等)全都提供多架构版本,不用操心。需要警惕的是小众镜像和自己以前构建的单架构镜像。检查一个镜像支持哪些平台:

Terminal window
docker buildx imagetools inspect python:3.12-slim

看它 Manifests 里列出的 Platform 即可。

下载二进制时用 TARGETARCH 按架构选文件#

最典型的翻车场景:Dockerfile 里写死了下载 amd64 版本的工具(比如某个工具的 release 地址里带 amd64 字样),单架构时没问题,构建 arm64 版时下回来的还是 amd64 二进制,装的时候不报错,一运行就 exec format error,而且是在容器启动时才爆,排查起来很绕。

buildx 在构建时会自动注入一组平台相关的构建参数,其中最常用的是 TARGETARCH(目标架构,取值 amd64/arm64)。在 Dockerfile 里声明并用它拼下载地址即可:

FROM alpine:3.20
ARG TARGETARCH
ARG TOOL_VERSION=1.2.3
RUN apk add --no-cache curl \
&& curl -fsSL -o /usr/local/bin/tool \
"https://example.com/releases/v${TOOL_VERSION}/tool-linux-${TARGETARCH}" \
&& chmod +x /usr/local/bin/tool
CMD ["tool", "serve"]

构建哪个平台,TARGETARCH 就是哪个平台的值,下载的永远是对应架构的文件。同系列的自动参数还有几个,了解即可:

构建参数含义例子
TARGETPLATFORM目标平台完整写法linux/arm64
TARGETOS目标系统linux
TARGETARCH目标架构amd64、arm64
TARGETVARIANT架构变体v7(如 linux/arm/v7)
BUILDPLATFORM构建机自身的平台linux/amd64
WARNING

这类自动参数必须先用 ARG 声明才能在 Dockerfile 里引用,忘了声明会取到空值,下载地址拼错还不容易发现。另外下载前最好核对上游 release 的命名规则:有的项目 arm64 文件叫 aarch64,有的叫 arm64,名字对不上会直接 404,按对方的实际文件名写映射即可。

BUILDPLATFORM 还有个进阶用法:编译型语言可以让编译步骤固定在构建机原生架构上跑(速度快),只把产物按目标架构交叉编译,典型写法是 FROM --platform=$BUILDPLATFORM golang:1.23 AS builder。Go 这类自带交叉编译的语言特别适合这么干,能绕开 QEMU 的慢,这属于优化手段,先了解有这么回事即可。

QEMU 模拟的性能坑与原生构建节点#

前面说过 QEMU 是模拟执行,它的代价要心里有数:

  • 纯计算和编译任务最慢:模拟下 CPU 密集型操作的耗时常常是原生的 5–10 倍,大型项目一次构建几十分钟很常见;
  • I/O 和脚本类步骤影响相对小:拷贝文件、解压、跑简单脚本还能接受;
  • 构建缓存能救命:依赖层没变化时直接命中缓存,慢的主要是首次构建和代码层。

如果双架构构建要进日常流水线,正经解法是加一台 ARM 原生构建节点:准备一台 ARM 机器(ARM 云主机、树莓派都行),让 buildx 把 arm64 的构建任务分发到它上面原生执行,amd64 的留在本机,两个平台真正并行、还都是原生速度:

Terminal window
# 本机 builder 已创建的前提下,把远程 ARM 机器追加为节点
docker buildx create --name multi --append ssh://user@arm-host

追加后用 docker buildx inspect --bootstrap 能看到 builder 底下挂了两个节点、各负责不同平台。前提是本机能免密 SSH 登录那台 ARM 机器,且对方装了 Docker。这个方案要多维护一台机器,属于团队/高频构建场景的选择;个人项目偶尔构建,用 QEMU 忍一下慢通常就够了。核心结论是:先用 QEMU 把多架构跑通,慢到受不了再上原生节点,不要一上来就折腾集群。

给现有项目补上双架构:改造清单#

如果你手里已经有一批按老办法单架构发布的自建镜像,按下面这个顺序改造,半小时内就能切换到双架构发布,不需要推翻任何现有流程:

  1. 盘点基础镜像:把每个项目的 Dockerfile 翻出来,用 imagetools inspect 逐个确认 FROM 的基础镜像有没有 arm64 版本。官方主流镜像基本都有;卡住的一般是小众镜像,处理办法是换同类官方基础镜像,或者自己先给它做一份双架构版;
  2. 清理写死架构的地方:全局搜 Dockerfile 里的 amd64、x86_64 字样,凡是下载地址、文件名里带架构的,全部改成用 TARGETARCH 拼接(见前面专节);
  3. 本地跑通一次:装 QEMU、建 docker-container 驱动的 builder,在测试标签(比如 1.1-rc1)上完整走一遍构建、推送、双平台验证,确认没问题再动正式标签;
  4. 切换正式发布命令:原来的 docker build+docker push 两条命令,替换成本篇的 docker buildx build --platform ... --push 一条。镜像名和标签规则保持不变,使用者那边零感知;
  5. 老标签不用管:以前发布的单架构老版本标签就留在仓库里,不影响新版本;从切换后的第一个版本起,标签自动变双架构。

如果你的构建是在 GitHub Actions 里跑的,改造量同样很小:官方提供了现成的动作,流水线里依次加上 docker/setup-qemu-action(装模拟)、docker/setup-buildx-action(建 builder)、docker/build-push-action(构建推送时把 platforms 参数填上两个平台)即可,逻辑和本地完全一致。云端跑 QEMU 构建的速度同样不快,项目大了记得给流水线留足超时时间,或者只在打正式标签时才触发双架构构建、日常提交只做单架构快速验证,这是很多开源项目在用的折中办法。

排障#

容器一启动就报 exec format error。 架构不对的头号症状。按顺序查:①docker image inspect 镜像 --format '{{.Architecture}}' 看拉到的镜像架构和 uname -m 看本机架构是否一致;②镜像是自己构建的,检查构建时有没有带全 --platform;③镜像架构没错还报错,多半是镜像内部某个二进制选错了架构,回头查 Dockerfile 里有没有写死架构的下载(见 TARGETARCH 一节)。

构建 arm64 步骤时报 exec format error 或卡死在 RUN 步骤。 基本是 QEMU 没装或注册失效(宿主机重启后会失效)。重新执行 docker run --privileged --rm tonistiigi/binfmt --install all,再 docker buildx inspect --bootstrap 确认 Platforms 里有 linux/arm64。

docker buildx build 提示当前 builder 不支持多平台。 你多半还在用默认的 docker 驱动 builder。执行 docker buildx create --name multi --driver docker-container --use 切换过去再构建。

多平台加 --load 报错 docker exporter does not currently support exporting manifest lists。 预期行为,不是故障。多平台结果改用 --push 推仓库,再从仓库拉回来验证。

推送时报 denied 或 unauthorized。 仓库没登录或没权限。先 docker login 登录目标仓库;镜像名里的用户名/组织名要和登录账号对得上,个人镜像写成别人的命名空间是推不上去的。

--platform 写了但只构建出一个架构。 检查写法:平台之间是英文逗号分隔、不能有空格,格式必须是 linux/arm64 这种「系统/架构」全称,只写 arm64 不行。改完用 imagetools inspect 复查清单里平台数对不对。

某一平台构建中途失败,另一平台成功。 buildx 默认整体失败退出。先看失败平台卡在哪一步:基础镜像缺该平台版本、下载地址分架构 404、模拟执行超时,这三类最常见,分别对应前面各节的处理办法。

小结#

  • 镜像分架构,amd64 的镜像在 ARM 上启动必然 exec format error,多架构镜像是唯一的正经解法;
  • 原理是 manifest list:一份标签挂多份架构 manifest,拉取时按本机架构自动选;
  • 流程四步:装 QEMU(tonistiigi/binfmt --install all)→ 创建 docker-container 驱动的 builder → docker buildx build --platform linux/amd64,linux/arm64 --push → imagetools inspect 验证清单;
  • 多平台结果不能 --load,只能推仓库,这是机制决定的;
  • Dockerfile 里下载二进制务必用 TARGETARCH 按架构选文件,基础镜像也要先确认有多架构版本;
  • QEMU 够用但慢,高频构建再考虑加 ARM 原生节点,别提前过度折腾。
多平台镜像构建实战:buildx 双架构
https://blog.anexns.com/posts/docker-buildx-multiarch/
作者
Anexns
发布于
2026-11-15
许可协议
CC BY-NC-SA 4.0