Docker buildx 实现跨架构构建与多架构镜像

当构建机是 arm64,而生产环境是 amd64 时,如何让 CI/CD 产出的镜像在两种架构上都能直接
docker run?最简单的答案是:使用buildx,让QEMU/binfmt_misc 负责跨架构执行,manifest list 让同一个 tag 对应多个架构,私有 Registry 负责承接与分发。本文从原理到实操把这条链路完整走一遍,并记录过程中的若干踩坑。
背景
本网站是基于 Python 自研的,使用 docker compose 的方式部署。家里的 Homelab 是 arm64 架构,部署了测试环境;而运行网站的正式环境 VPS 是 amd64 架构。因此,在 Homelab 的 CI/CD 上需要进行交叉编译。
本文将讨论基于 Docker 的跨架构编译、运行方法。
基本原理
Docker 最简单的跨架构构建和运行容器的方式,是使用 QEMU 用户态模拟来解释执行目标架构的二进制,也就是最常使用的 buildx 机制,其底层依赖 QEMU、binfmt_misc 等技术。
QEMU 用户态模拟不需要启动 QEMU 系统级虚拟机或 KVM 虚拟化,但性能比真实环境慢 5~20 倍。对于 CI/CD 中偶尔的跨架构构建来说完全够用,但不适合长期以模拟方式运行服务。
binfmt_misc 是 Linux 内核提供的"杂项二进制格式"机制:可以向内核注册规则——当进程 exec 加
载的文件头部(magic)匹配某个特征时,内核不再报 exec format error,而是把文件交给规则指定的解释器执行。跨架构场景下,tonistiigi/binfmt 做的就是把 qemu-x86_64 这类模拟器注册进内核,此后进程执行 amd64 二进制时会被透明地转交给 QEMU 解释执行,进程自身毫无感知。注册结果保存在内核中(/proc/sys/fs/binfmt_misc/),全局生效、对容器透明,所以安装命令需要 --privileged,而注册一次后所有容器都能直接运行跨架构镜像;注册时的 fix-binary 标志还会让解释器常驻内存,即使容器镜像里没有 qemu 二进制(如 python:3.14-slim)也能正常模拟执行。
准备 buildx 环境
本方案需要提前安装好 Docker,我的实验环境为:Docker CE 29.5.3,系统为 Debian 12(arm64)。Docker CE 的安装方法可以参考:https://mirrors.zju.edu.cn/docker-ce/
安装 buildx 插件
要使用 buildx,需要先安装 docker-buildx-plugin(较新版本的 Docker CE 已自带):
sudo apt install -y docker-buildx-plugin
安装后,buildx 默认只支持本机架构,可以使用如下命令查看:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.30.0 linux/arm64, linux/arm (+2)
可以看到 Docker 已经自带了一个名为 default 的 builder。用户也可以使用 docker buildx create ... 命令创建自己的 builder,但一般情况下没必要。
添加 amd64 模拟支持
借助 tonistiigi/binfmt 镜像,可以向内核注册额外的架构模拟器:
$ # 检查 tonistiigi/binfmt 版本(初次运行会自动拉取 tonistiigi/binfmt 镜像)
$ docker run --privileged --rm tonistiigi/binfmt --version
binfmt/e29e7d7 qemu/v10.2.3 go/1.26.4
$ docker run --privileged --rm tonistiigi/binfmt --install linux/amd64
installing: amd64 OK
{
"supported": [
"linux/arm64",
"linux/amd64",
"linux/amd64/v2",
"linux/arm/v7",
"linux/arm/v6"
],
"emulators": [
"python3.11",
"qemu-x86_64"
]
}
$ docker buildx ls # 再次检查支持的架构
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.30.0 linux/amd64 (+2), linux/arm64, linux/arm (+2)
可以看到,安装后已经支持 amd64 了。
tonistiigi/binfmt也支持--install all,可安装包括riscv在内的所有架构。
运行跨架构镜像
安装好模拟器后,先验证跨架构运行。注意:运行跨架构容器只依赖 binfmt 模拟器,不需要修改 Docker 的任何配置。
运行默认架构(arm64)的镜像:
$ docker run --rm python:3.14-slim python -c 'import platform;print(platform.machine())'
aarch64
在 arm64 上运行 amd64 架构的镜像:
$ docker run --platform=linux/amd64 --rm python:3.14-slim python -c 'import platform;print(platform.machine())'
x86_64
自建 Docker Registry
在 Docker 官方镜像仓库中,可以看到许多镜像的一个 tag 同时支持多种 CPU 架构。这里我们使用官方的 registry 镜像自建一个私有仓库,探索这是如何实现的。
编写 compose.yaml
创建如下的 compose.yaml 文件:
services:
registry-server:
restart: always
image: registry:2
container_name: registry-server
volumes:
- ./config.yml:/etc/docker/registry/config.yml
- ./htpasswd:/tmp/htpasswd
- ./docker-registry:/var/lib/registry
registry-ui:
image: joxit/docker-registry-ui:main
container_name: registry-ui
restart: always
ports:
- 5000:80
environment:
- SINGLE_REGISTRY=true
- DELETE_IMAGES=true
- SHOW_CONTENT_DIGEST=true
- NGINX_PROXY_PASS_URL=http://registry-server:5000
- SHOW_CATALOG_NB_TAGS=true
- CATALOG_MIN_BRANCHES=1
- CATALOG_MAX_BRANCHES=1
- TAGLIST_PAGE_SIZE=100
- REGISTRY_SECURED=false
- CATALOG_ELEMENTS_LIMIT=1000
其中:
registry:2是 Docker 官方开源的镜像仓库服务;joxit/docker-registry-ui是一个极简的 Docker 镜像仓库 UI,项目地址:https://github.com/joxit/docker-registry-ui。
踩坑提示:不要在 compose 的
environment里用REGISTRY_*环境变量重复配置 config.yml 已有的选项,环境变量的优先级高于配置文件。例如设置REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data会覆盖配置文件中的rootdirectory配置项。本文把 registry 的配置统一放在 config.yml 中。
生成认证文件
Registry 使用 HTTP Basic 认证,凭据保存在 htpasswd 文件中(用户名 admin,密码 use-strong-password):
# 方式一:使用 htpasswd 工具
$ htpasswd -Bbn admin use-strong-password > htpasswd
# 方式二:使用 Python 的 bcrypt 库
$ pip install bcrypt
$ python -c 'import bcrypt; user="admin";passwd="use-strong-password"; print("%s:%s" % (user,bcrypt.hashpw(passwd.encode(), bcrypt.gensalt()).decode()))' > htpasswd
编写 Registry 配置文件
创建配置文件 config.yml:
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
delete:
enabled: true
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
auth:
htpasswd:
realm: basic-realm
path: /tmp/htpasswd
Registry 的详细配置方法见:https://distribution.github.io/distribution/about/configuration/。
Registry 不仅支持上传镜像,也支持作为 proxy 代理,拉取并缓存远端镜像仓库的镜像。在国内访问 Docker Hub 不太稳定的环境下,这个功能十分有用。
启动并登录
启动这个私有镜像仓库:
$ docker compose up -d
通过 http://127.0.0.1:5000 访问自建的 Registry UI,密码就是上面生成的密码。
为了让 Docker 能使用这个 HTTP 私有仓库,还需要在 /etc/docker/daemon.json 中将其加入 insecure-registries:
{
"insecure-registries": [
"127.0.0.1:5000"
]
}
然后重启 Docker:
$ systemctl restart docker
接着登录,输入用户名和密码:
$ docker login 127.0.0.1:5000
登录成功后,就可以往这个自建的镜像仓库里上传和下载镜像了。也可以用浏览器访问 http://127.0.0.1:5000 浏览已上传的镜像。
构建多架构镜像
多架构镜像的基本原理:manifest list
利用 manifest list,可以把不同架构的镜像用同一个 tag 对外暴露。下面用前文运行过的 python:3.14-slim 的两个平台版本,实操演示如何手工制作一个多架构镜像。
第一步,依次拉取两个平台的镜像,并在每次拉取后立即用 docker tag 重命名到私有仓库名下:
$ docker pull --platform=linux/arm64 python:3.14-slim
$ docker tag python:3.14-slim 127.0.0.1:5000/library/python:3.14-slim-arm64
$ docker pull --platform=linux/amd64 python:3.14-slim
$ docker tag python:3.14-slim 127.0.0.1:5000/library/python:3.14-slim-amd64
为什么每次 pull 之后要立即 tag?经典镜像存储下,同一个 tag 只能对应一个镜像,第二次 pull 会直接替换
python:3.14-slim在本地指向的镜像,先改名才能把前一个平台"保住"。arm64 主机上第一次 pull 也可以不写--platform,默认就是 arm64。
改名后,本地两个镜像的 IMAGE ID 不同,各自对应一个平台:
$ docker image ls | grep python
127.0.0.1:5000/library/python:3.14-slim-amd64 9b84b0c75adf 128MB 0B
127.0.0.1:5000/library/python:3.14-slim-arm64 19fe7a28429c 153MB 0B
python:3.14-slim 9b84b0c75adf 128MB 0B
推送到私有仓库:
$ docker push 127.0.0.1:5000/library/python:3.14-slim-arm64
$ docker push 127.0.0.1:5000/library/python:3.14-slim-amd64
第二步,用 docker manifest 把两个单架构镜像合并为一个 manifest list(后两个参数是待合并的镜像列表):
$ docker manifest create --insecure 127.0.0.1:5000/library/python:3.14-slim \
127.0.0.1:5000/library/python:3.14-slim-arm64 \
127.0.0.1:5000/library/python:3.14-slim-amd64
Created manifest list 127.0.0.1:5000/library/python:3.14-slim
$ docker manifest push --insecure 127.0.0.1:5000/library/python:3.14-slim
验证。docker manifest inspect 直接查看 manifest list 的内容,比运行容器更直观:
$ docker manifest inspect --insecure 127.0.0.1:5000/library/python:3.14-slim
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
"manifests": [
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 1159,
"digest": "sha256:610015ff...",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"size": 1159,
"digest": "sha256:c71cd015...",
"platform": {
"architecture": "arm64",
"os": "linux",
"variant": "v8"
}
}
]
}
打开 Web UI 能看到上传的镜像情况:

上图中,前两个是不同架构的独立镜像,第三个是合并两个架构后的 manifest 镜像。
也可以运行验证(arm64 主机上):
$ docker run --rm 127.0.0.1:5000/library/python:3.14-slim python -c 'import platform;print(platform.machine())'
aarch64
$ docker run --rm --platform=linux/amd64 127.0.0.1:5000/library/python:3.14-slim python -c 'import platform;print(platform.machine())'
x86_64
换用其他机器,在不指定 --platform 的情况下,Docker 会自动 pull 对应架构的镜像。这样就实现了一个 tag 支持多个架构。
实操中有几个坑值得记录:
- 私有仓库没有配置 HTTPS,
docker manifest的create/push等命令必须加--insecure,否则报No such manifest: xxx。manifest子命令由 CLI 直连 registry 操作,不走 daemon 的insecure-registries配置; docker manifest create的结果先缓存在 CLI 本地,对同一个 tag 重复执行 create 会报refusing to amend an existing manifest list with no --amend flag,此时可以加--amend追加,或先用docker manifest rm <tag>清除本地缓存;- 在开启 containerd 镜像存储的机器上,
docker pull --platform拉取后 tag 实际指向完整的 manifest list(内容按需加载),docker tag+docker push无法按平台拆分推送,Docker 会回退为只推送本机平台的镜像。
开启 containerd 镜像存储
上面的流程适用于经典镜像存储(graph driver)。经典镜像存储使用 Docker 私有的镜像格式,同一个 tag 只能对应一个平台的镜像,这个限制同样让默认的 docker driver 无法直接构建多平台镜像。Docker 可以通过配置开启 containerd 镜像存储:镜像改用与 registry、独立 containerd 一致的 OCI 格式存储,一个 tag 可以对应多个平台的镜像,之后就可以使用 buildx 一次构建多个架构的镜像。
在 graph driver 格式的情况下,buildx 构建多架构镜像会报错: ERROR: failed to build: Multi-platform build is not supported for the docker driver. Switch to a different driver, or turn on the containerd image store, and try again. Learn more at https://docs.docker.com/go/build-multi-platform/
修改 Docker 的配置文件 /etc/docker/daemon.json:
{
...
"features": {
"containerd-snapshotter": true
}
}
注意:开启后 Docker 会切换镜像存储格式,所有已有镜像将不可见;回退该配置并重启后即可恢复。
由于镜像格式不兼容,切换存储后原有镜像对 Docker 不可见,正在运行的镜像中心容器也会失效。因此先关闭镜像中心,再重启 Docker:
$ docker compose down
$ systemctl restart docker
开启后,本地一个 tag 就可以同时保存多个平台的镜像,这也是下一节构建多架构镜像的前提。
使用 buildx 一键构建多架构镜像
Docker Buildx 是 Docker 的一个扩展工具,提供了更高级的构建功能。它基于 Moby 项目的 BuildKit,支持多架构构建、并行构建等特性。
上一节我们用现成的镜像手工合并 manifest list,这正是多架构镜像的本质。在实际的 CI/CD 中,更常用的方式是让 buildx 一次完成构建和合并:各个平台分别构建,最后自动生成 manifest list 并推送。
重新启动 Docker Registry 服务:
$ docker compose up -d
创建一个 Dockerfile:
FROM python:3.14-slim
ENTRYPOINT ["python", "-c", "import platform;print(platform.machine())"]
构建多架构的镜像并推送到私有仓库:
$ docker buildx build --platform linux/amd64,linux/arm64 \
-t 127.0.0.1:5000/mybuild/python-arch-test:1.0.0 . --push
这个命令会根据 Dockerfile 生成 amd64 和 arm64 两个架构的镜像,并把它们合并为一个 manifest list 推送到目标镜像仓库。
验证:
$ docker run --rm 127.0.0.1:5000/mybuild/python-arch-test:1.0.0
aarch64
$ docker run --rm --platform=linux/amd64 127.0.0.1:5000/mybuild/python-arch-test:1.0.0
x86_64
登录 Docker Registry 的 UI,查看刚刚上传的镜像列表:

扩展阅读:为 buildx 添加 docker-container driver
Docker 默认的 buildx driver 是 docker,它依赖 Docker 自身的镜像存储,传统上每个 tag 只能对应一份镜像。如果不想(或无法)开启 containerd 镜像存储,可以创建一个以 docker-container 为 driver 的 builder,由独立的 BuildKit 容器完成多架构构建。创建名为 multi-arch 的 builder:
$ docker buildx create --name multi-arch --driver docker-container --bootstrap --use
检查:
$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
multi-arch* docker-container
\_ multi-arch0 \_ unix:///var/run/docker.sock running v0.30.0 linux/arm64, linux/amd64, linux/amd64/v2, linux/arm/v7, linux/arm/v6
default docker
\_ default \_ default running v0.30.0 linux/arm64, linux/amd64, linux/amd64/v2, linux/arm/v7, linux/arm/v6
现在有了 2 个 builder,新建的 multi-arch 已经是默认 builder,driver 是 docker-container。
docker-container 这个 driver 会在独立容器中运行自带 containerd 的 BuildKit 服务,其构建缓存和镜像与 Docker 本身相互独立、互不通用。构建缓存保存在 builder 容器内部,容器删除即丢失;CI 场景可以用 --cache-to/--cache-from 把缓存导出到 registry 等外部后端,下次构建时复用,可帮助 QEMU 命中缓存后跳过某些费时的步骤。
builder 还支持由多个节点组成(docker buildx create --append 追加节点),buildx 会按 platform 把构建分发到对应架构的节点原生执行,最后仍汇总为一个 manifest list 推送——这是绕开 QEMU 模拟(慢且偶发崩溃)的根治办法,代价是需要多台构建机和 SSH 互通。
总结
- QEMU 用户态模拟(binfmt_misc)让 arm64 设备可以构建、运行 amd64 镜像,代价是 5~20 倍的性能损耗,适合 CI/CD 场景;
- 开启 containerd 镜像存储(
containerd-snapshotter)后,默认的dockerdriver 也能构建多架构镜像,且本地一个 tag 可保存多个平台的镜像; - 多架构镜像的本质是 manifest list:多个架构的镜像共享一个 tag,客户端按需拉取;
- 也可以用
docker manifest create/push手动合并已存在的多架构镜像,或使用docker-containerdriver 的独立 builder 完成多架构构建。
Last updates at Sep 09,2026
Views (13)
total 0 comments