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

Sep 09,2026 linux Docker buildx

missing-spring

当构建机是 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

其中:

踩坑提示:不要在 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 能看到上传的镜像情况:

registry-manifest.png

上图中,前两个是不同架构的独立镜像,第三个是合并两个架构后的 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 manifestcreate/push 等命令必须加 --insecure,否则报 No such manifest: xxxmanifest 子命令由 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,查看刚刚上传的镜像列表:

registry-ui-buildx.png

扩展阅读:为 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)后,默认的 docker driver 也能构建多架构镜像,且本地一个 tag 可保存多个平台的镜像;
  • 多架构镜像的本质是 manifest list:多个架构的镜像共享一个 tag,客户端按需拉取;
  • 也可以用 docker manifest create/push 手动合并已存在的多架构镜像,或使用 docker-container driver 的独立 builder 完成多架构构建。

Last updates at Sep 09,2026

Views (13)

Leave a Comment

total 0 comments