Docker 基础体系 · 第 7/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 多架构构建:Buildx、QEMU、Manifest 与跨平台发布

“多架构镜像”不是把一个二进制文件同时标记成多个架构,而是为不同目标平台分别构建镜像,并让镜像仓库通过一个统一入口选择正确的镜像变体。

例如,linux/amd64linux/arm64 是两个不同的平台目标:

应用标签 app:1.0
└── 镜像索引 / Manifest List
    ├── linux/amd64  -> sha256:amd64-image-digest
    └── linux/arm64  -> sha256:arm64-image-digest

用户执行:

docker pull registry.example.com/demo/app:1.0

Docker Engine 会根据当前节点的平台选择其中一个子镜像。用户看到的是同一个 Tag,但实际下载的镜像内容可能完全不同。

本文使用现代 Docker Engine、BuildKit、Buildx 和 Compose 规范中的 Linux 容器场景。重点是 linux/amd64linux/arm64 等平台;Windows 容器使用不同的基础镜像、内核和运行时约束,不能简单套用本文结论。


一、先建立平台模型:OS、架构与变体

Docker 使用平台标识描述镜像适用的运行环境,常见形式为:

os/architecture[/variant]

例如:

linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6

其中:

  • os 表示操作系统,例如 linux
  • architecture 表示 CPU 架构,例如 amd64arm64arm
  • variant 表示同一架构下的变体,例如 ARMv7 的 v7

在 OCI Image Specification 中,平台信息还可以包含 os.versionos.features。实际 Docker 平台选择通常最关心操作系统、架构和架构变体。

可以把一个目标平台抽象为:

P=(OS,Arch,Variant)P = (OS, Arch, Variant)

一个镜像变体只有在其平台属性与运行节点兼容时,才适合作为该节点的运行镜像。例如:

linux/amd64 镜像

不能因为 Linux 用户态相同,就直接在没有模拟或原生支持的 ARM64 节点上运行。CPU 指令集不同,二进制文件中的 ELF machine 类型也不同。

查看当前 Docker 节点的平台:

docker version --format '{{.Server.Os}}/{{.Server.Arch}}'

典型输出:

linux/amd64

也可以查看 Buildx 当前构建器支持的平台:

docker buildx ls

输出可能类似:

NAME/NODE    DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMS
default      docker            running   v0.12.x    linux/amd64

这里的 PLATFORMS 表示构建器声明或检测到的能力,不等于每个平台都一定以原生 CPU 速度执行。某些平台可能依赖 QEMU 模拟。


二、Buildx、BuildKit 与 Docker Engine 的关系

2.1 BuildKit 是构建后端

BuildKit 是 Docker 现代镜像构建系统。它负责:

  1. 解析 Dockerfile;
  2. 处理构建上下文;
  3. 执行每个构建阶段;
  4. 管理构建缓存;
  5. 处理 Secret、SSH 等构建会话;
  6. 生成镜像配置、文件层和最终镜像;
  7. 将结果输出到本地镜像存储、OCI 文件、Docker 镜像格式或 Registry。

BuildKit 不仅执行 RUN,还会根据 Dockerfile 的依赖关系构建一张内部的构建图。没有依赖关系的步骤可能被并行处理,缓存命中时也可以跳过实际执行。

2.2 Buildx 是构建器管理与调用接口

Buildx 是 Docker CLI 的扩展插件,常见命令形式为:

docker buildx build ...

Buildx 主要解决以下问题:

  • 创建和切换 BuildKit 构建器;
  • 管理多个构建节点;
  • 指定 --platform
  • 选择构建输出类型;
  • 将多个平台的结果推送为一个统一的镜像索引;
  • 使用 imagetools 查看和操作远程镜像元数据。

因此,Buildx 本身不是编译器,也不是 CPU 模拟器。它负责组织构建任务;实际执行由 BuildKit 完成;当执行目标平台指令而宿主机架构不同,才可能由 QEMU 提供指令模拟。

2.3 Buildx 的 driver 决定 BuildKit 在哪里运行

常见 driver 包括:

Driver 特点
docker 使用 Docker Engine 内置的 BuildKit,配置简单
docker-container 在专用 BuildKit 容器中运行,隔离性和能力通常更好
kubernetes 将 BuildKit 运行在 Kubernetes 中
remote 连接到远程 BuildKit 服务

创建一个独立的 BuildKit 容器构建器:

docker buildx create \
  --name multi-builder \
  --driver docker-container \
  --use

docker buildx inspect --bootstrap

各命令的作用是:

  1. create 创建名为 multi-builder 的构建器;
  2. --driver docker-container 让 BuildKit 运行在专用容器中;
  3. --use 将当前 CLI 上下文切换到该构建器;
  4. inspect --bootstrap 启动并检查 BuildKit 节点。

预期可以看到类似信息:

Name:          multi-builder
Driver:        docker-container
Status:        running
Buildkit:      v0.x.x
Platforms:     linux/amd64, linux/arm64, ...

版本号会随 Docker 和 BuildKit 发行版本变化,不应把某个固定版本当作接口保证。


三、QEMU 到底解决了什么问题

3.1 QEMU 是用户态指令模拟

QEMU 在多架构构建中通常以用户态模拟的方式出现。假设:

构建节点:linux/amd64
目标平台:linux/arm64

Dockerfile 中的某一步运行了 ARM64 程序:

RUN ./configure
RUN make

如果这些程序是 ARM64 ELF 二进制,而构建节点 CPU 是 amd64,那么 amd64 CPU 不能直接执行它们。QEMU 可以把 ARM64 指令动态翻译为 amd64 可执行的指令,使这些程序在构建过程中运行。

它解决的是:

目标架构的用户态程序如何在当前架构 CPU 上执行

它没有解决:

  • 目标平台的 Linux 内核;
  • 目标架构的真实硬件特性;
  • 原生 CPU 性能;
  • 所有架构相关的内核行为;
  • 某些依赖真实设备或特殊指令的程序。

Linux 容器共享宿主机 Linux 内核。一个 linux/arm64 容器并没有携带一个 ARM64 Linux 内核。它使用的是宿主机内核提供的系统调用接口,QEMU 只负责处理用户态程序的 CPU 指令差异。

3.2 binfmt_misc 把二进制格式连接到 QEMU

Linux 内核的 binfmt_misc 机制可以根据 ELF 文件的架构标记,自动调用对应的解释器。例如,当系统发现一个 ARM64 ELF 文件时,可以将它交给 qemu-aarch64

在许多 Docker Desktop 环境中,相关能力已经预配置。Linux 主机上可以使用官方常见的 binfmt 安装方式:

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

这个命令需要:

  • Docker 能够运行特权容器;
  • 主机内核支持相关 binfmt_misc 配置;
  • 操作者有足够权限。

检查 BuildKit 能否看到多架构平台:

docker buildx inspect --bootstrap

也可以用一个 ARM64 容器验证运行链路:

docker run --rm --platform linux/arm64 alpine uname -m

在 amd64 主机并且 QEMU 配置正确时,常见输出为:

aarch64

这证明 ARM64 用户态程序可以通过当前 Docker 环境启动,但不代表所有 ARM64 程序都能无差异运行。

3.3 QEMU 的性能和故障边界

QEMU 模拟通常比原生执行慢,尤其是:

  • 编译器大量执行;
  • 链接大型程序;
  • 运行测试套件;
  • 进行压缩、加密或大量系统调用;
  • 构建依赖较多的语言生态。

常见失败表现包括:

exec format error

可能原因有:

  1. 目标架构二进制被送到了没有对应 QEMU 的节点;
  2. binfmt_misc 没有配置;
  3. 镜像平台声明与实际二进制不一致;
  4. 脚本的解释器不存在;
  5. 脚本使用了错误的换行格式,例如从 Windows 工作区复制了 CRLF;
  6. 程序使用了模拟器不支持或表现不同的指令。

因此,“安装了 QEMU”不等于“所有跨架构构建都可靠”。如果可以原生编译,通常应优先采用原生构建节点或交叉编译。


四、三种多架构构建策略

BuildKit 常见的多架构方案可以分为三类。

4.1 QEMU 模拟执行

构建任务在一个节点上完成,目标平台的 RUN 步骤通过 QEMU 执行。

amd64 BuildKit
└── 构建 linux/arm64
    └── ARM64 RUN 程序由 QEMU 翻译执行

优点是配置简单,Dockerfile 改动少。缺点是构建速度、兼容性和诊断难度通常不如原生构建。

4.2 多个原生构建节点

为不同架构准备不同节点:

amd64 BuildKit 节点 -> 构建 linux/amd64
arm64 BuildKit 节点 -> 构建 linux/arm64

可以通过 Docker contexts 将节点加入同一个 Buildx builder。先创建或配置连接到不同机器的 context,再执行:

docker buildx create \
  --name native-builder \
  --driver docker-container \
  amd64-context

docker buildx create \
  --name native-builder \
  --append \
  arm64-context

docker buildx use native-builder
docker buildx inspect --bootstrap

这里 amd64-contextarm64-context 是预先创建的 Docker context 名称。BuildKit 根据目标平台把任务调度到合适节点。

这种方式的优势是:

  • 编译和测试接近真实目标 CPU;
  • 减少 QEMU 兼容性问题;
  • 性能通常更稳定。

代价是需要维护多个节点、网络连接、凭据、缓存和版本一致性。

4.3 交叉编译

交叉编译使用当前架构的编译器生成目标架构二进制。例如,在 amd64 主机上运行 amd64 的 Go 编译器,但生成 ARM64 程序。

amd64 编译器
└── 生成 arm64 二进制

此时编译器本身不需要通过 QEMU 运行。只有当 Dockerfile 中的构建步骤需要执行目标架构程序时,才可能仍然需要模拟或原生节点。

三种方案不是互斥的。实际项目常见组合是:

原生或当前架构编译器负责交叉编译
+
QEMU 只执行少量目标架构工具
+
原生目标节点执行测试

五、Dockerfile 中的构建平台与目标平台

BuildKit 会提供一组自动平台参数:

  • BUILDPLATFORM:执行当前构建步骤的构建平台;
  • BUILDOS:构建平台的操作系统;
  • BUILDARCH:构建平台的架构;
  • BUILDVARIANT:构建平台的架构变体;
  • TARGETPLATFORM:最终目标平台;
  • TARGETOS:目标操作系统;
  • TARGETARCH:目标架构;
  • TARGETVARIANT:目标架构变体。

这些参数不是普通环境变量。要在 Dockerfile 的 RUN 中使用,必须在相应阶段声明:

ARG TARGETOS
ARG TARGETARCH

5.1 一个可运行的 Go 多架构 Dockerfile

项目目录:

.
├── Dockerfile
└── main.go

main.go

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Printf("hello from %s/%s\n", runtime.GOOS, runtime.GOARCH)
}

Dockerfile

# syntax=docker/dockerfile:1

FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS build

ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT

WORKDIR /src
COPY main.go .

RUN set -eux; \
    export GOOS="$TARGETOS"; \
    export GOARCH="$TARGETARCH"; \
    if [ "$TARGETARCH" = "arm" ] && [ -n "${TARGETVARIANT:-}" ]; then \
        export GOARM="${TARGETVARIANT#v}"; \
    fi; \
    CGO_ENABLED=0 go build -trimpath -o /out/app main.go

FROM alpine:3.20

COPY --from=build /out/app /usr/local/bin/app
ENTRYPOINT ["/usr/local/bin/app"]

构建并推送两个平台:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/demo/app:1.0 \
  --push \
  .

这里发生了两次目标构建:

目标一:TARGETPLATFORM=linux/amd64
目标二:TARGETPLATFORM=linux/arm64

每次构建的 golang 编译器都来自 BUILDPLATFORM

FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS build

然后通过 GOOSGOARCH 产生对应目标二进制。这样做的关键原因是:编译器不需要以目标架构执行,减少了对 QEMU 的依赖。

运行时阶段没有显式写 --platform

FROM alpine:3.20

BuildKit 会为每个目标平台选择对应的 Alpine 基础镜像。因此最终得到两个不同的运行时镜像。

5.2 为什么不能把所有阶段都固定为 BUILDPLATFORM

下面这种写法会让运行时基础镜像也固定为构建平台:

FROM --platform=$BUILDPLATFORM alpine:3.20

如果目标是 linux/arm64,这会得到 amd64 的运行时文件系统,却被放入 ARM64 目标构建流程中。最终可能出现:

exec format error

或者镜像元数据与实际内容不匹配。

FROM --platform=$BUILDPLATFORM 适合用在“编译器或工具链阶段”,不应无条件用于最终运行时阶段。最终阶段通常应让 BuildKit 根据目标平台选择基础镜像。

5.3 CGO 和本地依赖改变了问题

上面的 Go 示例使用:

CGO_ENABLED=0

这使程序尽量避免依赖目标平台的 C 库。若启用 CGO,则需要目标平台的 C 编译器、头文件和链接器,例如:

amd64 -> arm64 交叉编译器

还需要确认动态链接库来自目标平台,而不是构建平台。否则可能生成一个架构正确但运行时缺少正确 ABI 的程序。

交叉编译只能解决“生成目标二进制”的问题,不能自动解决:

  • 目标平台的 C 库版本;
  • 外部系统库;
  • 内核行为;
  • 运行时测试;
  • 需要执行目标程序的生成器工具;
  • 依赖真实 CPU 特性的代码。

六、Manifest 与镜像索引:一个 Tag 如何指向多个镜像

6.1 单个平台镜像的结构

一个容器镜像通常至少包含:

  1. 配置对象;
  2. 一个或多个文件系统层;
  3. 镜像清单(manifest)。

清单记录配置摘要和层摘要。例如,抽象后可以表示为:

{
  "schemaVersion": 2,
  "config": {
    "digest": "sha256:config..."
  },
  "layers": [
    { "digest": "sha256:layer-1..." },
    { "digest": "sha256:layer-2..." }
  ]
}

摘要是内容寻址标识:

digest=SHA256(content)digest = SHA256(content)

同样内容会产生同样摘要;内容变化会导致摘要变化。

6.2 多平台入口是 Manifest List 或 OCI Image Index

多平台入口不是一个包含所有文件层的普通镜像清单,而是一个指向多个平台镜像清单的上层对象。

Docker 生态常称其为 Manifest List,OCI 规范中对应的概念通常称为 Image Index。它大致包含:

{
  "manifests": [
    {
      "mediaType": "...",
      "digest": "sha256:amd64...",
      "platform": {
        "os": "linux",
        "architecture": "amd64"
      }
    },
    {
      "mediaType": "...",
      "digest": "sha256:arm64...",
      "platform": {
        "os": "linux",
        "architecture": "arm64"
      }
    }
  ]
}

完整发布关系是:

Tag app:1.0
  └── index digest
      ├── linux/amd64 manifest digest
      │   ├── config digest
      │   └── layer digests
      └── linux/arm64 manifest digest
          ├── config digest
          └── layer digests

因此有三个容易混淆的对象:

  • Tag:可变的名称,例如 app:1.0
  • Index 或 Manifest digest:多平台入口的不可变内容摘要;
  • 平台镜像 digest:某一个平台子镜像的不可变内容摘要。

6.3 使用 Buildx 一次性发布

推荐让 BuildKit 直接推送各平台镜像及其上层索引:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/demo/app:1.0 \
  --push \
  .

--platform 指定目标集合,-t 指定远程 Tag,--push 让构建结果进入 Registry。

构建完成后查看远程索引:

docker buildx imagetools inspect registry.example.com/demo/app:1.0

典型结果会列出:

Name:      registry.example.com/demo/app:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:...

Manifests:
  Name: ...@sha256:...
  Platform: linux/amd64

  Name: ...@sha256:...
  Platform: linux/arm64

实际 MediaType 可能是 OCI Image Index 或 Docker Manifest List,取决于构建器、镜像格式和 Registry 协商结果。查看时应以实际输出为准。

6.4 Tag 不是版本内容

下面两个引用的稳定性不同:

docker pull registry.example.com/demo/app:1.0
docker pull registry.example.com/demo/app@sha256:...

Tag 可以被重新推送:

app:1.0 -> index digest A
app:1.0 -> index digest B

Digest 则绑定具体内容。生产部署若要求可审计、可复现,通常应记录并部署 index digest,而不是只记录 Tag。

需要注意,多架构索引的 digest 与其中某个平台镜像的 digest 不同:

索引摘要:sha256:index...
amd64 子镜像:sha256:amd64...
arm64 子镜像:sha256:arm64...

客户端拉取索引后,会根据平台选择子镜像。若直接使用子镜像 digest,就绕过了多平台选择。


七、构建输出:为什么 --push 通常比 --load 更适合多架构

Buildx 构建结果必须指定输出位置或输出类型。常见方式包括:

7.1 推送到 Registry

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/demo/app:1.0 \
  --push \
  .

这是发布多架构镜像最直接的方式,因为 Registry 原生保存:

  • 各平台镜像清单;
  • 各平台层;
  • 上层 Image Index 或 Manifest List。

7.2 加载到本地 Docker 镜像存储

docker buildx build \
  --platform linux/amd64 \
  -t demo/app:test \
  --load \
  .

单平台结果通常可以加载到本地 Docker Engine,然后:

docker run --rm demo/app:test

传统 Docker image store 对多平台镜像的本地加载支持有限。执行:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t demo/app:test \
  --load \
  .

可能得到类似错误:

docker exporter does not currently support exporting manifest lists

确切错误文本会随版本变化。解决方法通常是:

  • 使用 --push 推送到 Registry;
  • 只构建一个平台并使用 --load
  • 使用启用了 containerd image store 的 Docker 环境;
  • 输出为 OCI 文件,再由支持该格式的工具处理。

不同 Docker Engine 版本对 containerd image store、多平台本地导入和 OCI 输出的支持存在差异,不应把某个本地存储行为当作所有环境的统一保证。

7.3 导出为 OCI 文件

例如:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --output type=oci,dest=app.oci.tar \
  .

该结果适合离线传递或由其他 OCI 工具处理,但它不是自动进入 Docker Engine 本地镜像列表。导入方式要根据目标工具和 Docker 版本确定。


八、完整的数据流与状态变化

一次多架构发布可以抽象为以下流程:

flowchart TD
    A[Dockerfile 与构建上下文] --> B[Buildx CLI]
    B --> C[BuildKit Builder]
    C --> D{目标平台}
    D -->|linux/amd64| E[amd64 构建节点或 QEMU]
    D -->|linux/arm64| F[arm64 构建节点或 QEMU]
    E --> G[amd64 镜像配置与层]
    F --> H[arm64 镜像配置与层]
    G --> I[平台镜像 Manifest]
    H --> J[平台镜像 Manifest]
    I --> K[OCI Index / Manifest List]
    J --> K
    K --> L[Registry Tag]
    L --> M[客户端按平台选择]

关键状态变化如下:

  1. Buildx 接收 --platform linux/amd64,linux/arm64
  2. BuildKit 为每个目标平台建立独立的构建结果;
  3. 各平台阶段产生各自的配置和文件层;
  4. 每个平台生成一个子镜像 Manifest;
  5. BuildKit 生成上层 Image Index 或 Manifest List;
  6. Registry 将索引和子镜像对象保存起来;
  7. docker pull 请求 Tag;
  8. Registry 返回索引;
  9. Docker Engine 使用自身平台选择子镜像;
  10. Engine 下载对应配置和层。

构建过程中的文件层不一定全部重复。不同平台的基础镜像层、编译产物和依赖通常不同;某些纯文本或架构无关层可能具有相同内容摘要,因此可以被 Registry 去重。


九、原生节点与 QEMU 的构建调度

如果一个 Buildx builder 有多个节点,BuildKit 会根据节点平台能力选择执行位置。可以检查节点:

docker buildx inspect native-builder

可能看到:

Name:          native-builder
Driver:        docker-container

Node:          native-builder0
Endpoint:      amd64-context
Platforms:     linux/amd64

Node:          native-builder1
Endpoint:      arm64-context
Platforms:     linux/arm64

在这种配置下:

docker buildx build \
  --builder native-builder \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/demo/app:1.0 \
  --push \
  .

理论上的调度关系是:

linux/amd64 -> native-builder0
linux/arm64 -> native-builder1

但实际任务还会受以下因素影响:

  • 节点是否在线;
  • 节点是否真的声明目标平台;
  • 某个阶段是否指定了 FROM --platform=$BUILDPLATFORM
  • 构建缓存是否存在于某个节点;
  • 使用的镜像是否能在该节点拉取;
  • Registry 凭据是否在对应节点可用。

多节点构建不是简单的“共享本地磁盘”。每个 BuildKit 节点有自己的缓存和运行环境。若需要共享缓存,应显式配置 Registry cache、对象存储或其他受支持的缓存后端,并验证其权限和并发行为。


十、Compose 中声明多架构构建与运行

Compose 可以在服务级别指定运行平台,也可以在构建配置中指定多个目标平台。

示例 compose.yaml

services:
  app:
    image: registry.example.com/demo/app:1.0
    build:
      context: .
      platforms:
        - linux/amd64
        - linux/arm64

构建并推送:

docker compose build --push app

如果 Compose 版本支持该命令和 build.platforms 配置,结果应为一个包含两个平台的远程镜像索引。

运行时不写 platform

docker compose up app

Docker 会根据当前节点选择匹配的平台。

如果明确要求在当前节点上运行 amd64 版本,可以写:

services:
  app:
    image: registry.example.com/demo/app:1.0
    platform: linux/amd64

这会改变运行平台选择,并可能触发 QEMU 模拟。它不是“声明这个服务天然支持 amd64”,而是要求 Compose/Docker 使用 amd64 镜像。

如果同时使用:

services:
  app:
    platform: linux/amd64
    build:
      context: .
      platforms:
        - linux/arm64

现代 Compose 实现通常会认为配置不一致,因为服务要求运行 amd64,却只构建 arm64。platformbuild.platforms 的兼容性需要满足当前 Compose 规范和实现校验规则。

还应区分:

build:
  platforms:
    - linux/amd64
    - linux/arm64

和:

platform: linux/amd64

前者描述构建输出的平台集合,后者描述该服务在 Compose 运行时应选择的平台。


十一、基础镜像、架构和镜像选择

多架构构建的前提是基础镜像本身提供目标平台变体。例如:

FROM alpine:3.20

当目标为 linux/amd64 时,BuildKit 解析到 amd64 变体;当目标为 linux/arm64 时,解析到 arm64 变体。

可以检查基础镜像是否具有多个平台:

docker buildx imagetools inspect alpine:3.20

如果基础镜像没有目标平台,构建可能失败:

no matching manifest for linux/arm64

此时不能仅靠修改最终镜像的 Architecture 字段解决问题。镜像中的文件层已经来自错误架构,元数据伪装只会把问题推迟到运行时。

尤其要注意以下误区:

FROM amd64-only-image

即使把最终 Tag 推送到一个看似多架构的索引中,仍然必须为 ARM64 构建出真实的 ARM64 文件系统和程序。一个 amd64 基础镜像不能被“重新标记”为 ARM64 镜像。


十二、构建缓存的多架构边界

BuildKit 缓存由 Dockerfile 指令、输入文件、依赖元数据和执行环境等因素共同决定。多架构构建时,不能假设所有平台完全共享缓存。

例如:

RUN go build ...

其输出依赖:

TARGETOS
TARGETARCH
TARGETVARIANT

amd64 和 arm64 的输出不同,因此应形成不同的有效缓存结果。

错误的缓存设计可能导致:

  • 用 amd64 构建产物覆盖 arm64 产物;
  • 不同架构共用不安全的编译器缓存目录;
  • 构建结果偶尔正确、偶尔错误;
  • 在 CI 并发构建时出现不可重复行为。

缓存挂载也要考虑平台隔离。例如:

RUN --mount=type=cache,target=/go/pkg/mod \
    go mod download

Go 模块下载缓存通常适合复用,但编译器对象缓存、链接器中间文件等内容可能包含架构相关结果。跨平台共享缓存时,应确认工具链自身是否保证安全;必要时使用带平台区分的缓存 ID,或让 BuildKit 的平台相关缓存键自然隔离。

构建缓存不等于发布结果。即使缓存命中,也必须验证最终平台镜像中的二进制架构。


十三、验证一个多架构镜像是否真的正确

13.1 检查索引的平台列表

docker buildx imagetools inspect \
  registry.example.com/demo/app:1.0

至少确认存在:

linux/amd64
linux/arm64

13.2 在对应平台运行程序

在 amd64 节点:

docker run --rm \
  --platform linux/amd64 \
  registry.example.com/demo/app:1.0

预期:

hello from linux/amd64

在 ARM64 节点:

docker run --rm \
  --platform linux/arm64 \
  registry.example.com/demo/app:1.0

预期:

hello from linux/arm64

如果在 amd64 主机上使用 --platform linux/arm64,该验证同时测试了 QEMU;如果要验证真正的 ARM64 运行环境,应在 ARM64 主机或 ARM64 CI 节点执行。

13.3 检查容器内二进制

进入镜像或临时容器后,可以使用:

file /usr/local/bin/app

典型结果:

ELF 64-bit LSB executable, ARM aarch64

或者:

ELF 64-bit LSB executable, x86-64

file 检查的是二进制实际内容;docker image inspect 主要查看镜像元数据。两者结合才能发现“元数据说是 ARM64,但文件实际是 amd64”这类错误。

13.4 验证固定 digest

先查看 Tag 对应的索引 digest:

docker buildx imagetools inspect \
  registry.example.com/demo/app:1.0

再使用 digest 拉取:

docker pull \
  registry.example.com/demo/app@sha256:<index-digest>

实际命令中的 <index-digest> 必须替换为 Registry 返回的完整摘要。部署系统应记录该摘要,而不是把文字占位符直接写入命令。


十四、常见失败路径与诊断方法

14.1 no matching manifest

表现:

no matching manifest for linux/arm64

含义通常是 Registry 返回的镜像索引中没有当前请求的平台,或者基础镜像不提供目标平台。

诊断步骤:

docker buildx imagetools inspect image:tag

如果是 Dockerfile 的基础镜像失败,则逐个检查每个 FROM 引用的镜像。多阶段构建中只要某个阶段的基础镜像不支持目标平台,就可能导致对应目标失败。

恢复方式:

  • 使用提供目标平台的基础镜像;
  • 更换基础镜像版本;
  • 为该平台单独构建基础镜像;
  • 删除不应支持的平台,不要伪造平台元数据。

14.2 exec format error

表现:

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

优先检查:

docker buildx imagetools inspect image:tag
file path/to/binary
docker buildx inspect --bootstrap

常见因果链是:

实际二进制为 amd64
+
容器或镜像被当作 arm64
+
ARM64 内核无法直接执行 amd64 指令
=
exec format error

如果宿主机确实需要通过 QEMU 运行,还要检查 binfmt 配置。若只是 shell 脚本,还要额外检查第一行解释器和换行符。

14.3 --load 无法导出多平台结果

表现可能类似:

docker exporter does not currently support exporting manifest lists

原因是当前 Docker 本地镜像存储或 exporter 不能接收一个包含多个平台的索引。

解决:

docker buildx build \
  --platform linux/amd64 \
  --load \
  -t demo/app:test \
  .

或者发布:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --push \
  -t registry.example.com/demo/app:test \
  .

14.4 QEMU 运行失败或构建极慢

如果出现非法指令、测试进程崩溃或构建时间明显增加,应区分三类问题:

  1. 模拟器或 binfmt 配置错误;
  2. 目标程序使用了模拟环境不支持的指令或行为;
  3. 构建本身需要大量目标架构执行。

诊断方式包括:

docker buildx inspect --bootstrap
docker run --rm --platform linux/arm64 alpine uname -m

如果基础验证成功但项目构建失败,尝试:

  • 将编译阶段固定为 BUILDPLATFORM
  • 使用目标架构交叉编译器;
  • 将测试移到原生目标节点;
  • 将大规模构建任务迁移到原生多节点 builder。

14.5 推送失败但本地构建成功

多架构推送需要 Registry 支持相应的 manifest/index 媒体类型,并且 BuildKit 节点需要正确的认证信息。

检查登录状态:

docker login registry.example.com

然后重试:

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/demo/app:1.0 \
  --push \
  .

多节点环境中,CLI 的登录凭据、BuildKit 节点访问 Registry 的网络路径和 Registry 权限可能不是同一层问题。某个节点能拉取基础镜像,不代表所有节点都能推送最终结果。

推送中断后,Registry 可能已经保存部分层或部分清单。通常可以重新执行同一个构建;内容寻址层会被复用,缺失对象会再次上传。清理未引用对象属于 Registry 的垃圾回收问题,不应直接删除仍被其他 Tag 引用的摘要。


十五、构建测试与运行测试不是一回事

多架构构建成功只能说明:

BuildKit 能够生成各平台的构建结果

它不自动证明:

各平台程序在真实目标环境中行为正确

例如,以下 Dockerfile 可能在构建时没有执行最终程序:

FROM --platform=$BUILDPLATFORM golang:1.23 AS build
RUN go build ...

构建过程成功,只表示交叉编译完成。程序是否能在 ARM64 运行,还需要实际启动:

docker run --rm --platform linux/arm64 image:tag

更可靠的 CI 结构通常分为两层:

构建阶段:
  为 amd64、arm64 分别生成镜像

验证阶段:
  在对应平台节点运行单元测试、启动测试和集成测试

如果测试本身依赖目标架构程序,则应让测试节点具备:

  • 原生目标 CPU;
  • 或正确工作的 QEMU;
  • 与生产接近的 Linux 内核和系统调用环境。

QEMU 可以作为开发和基础验证工具,但不应无条件替代生产架构上的测试证据。


十六、Linux 容器边界:架构正确不等于系统兼容

本文的 linux/amd64linux/arm64 组合包含两个维度:

操作系统:Linux
CPU 架构:amd64 或 arm64

容器镜像主要提供用户空间:

应用程序
动态链接器
共享库
配置文件
文件系统层

它不包含宿主机内核。因此:

  • linux/arm64 镜像需要 Linux 内核;
  • Linux 容器不能仅通过改变 platform 在 Windows 内核上直接运行;
  • Docker Desktop 通常通过 Linux VM 提供 Linux 容器所需的内核;
  • Windows 容器需要 Windows 基础镜像和 Windows 容器运行模式。

即使两个 Linux 架构都能启动,仍可能存在差异:

  • 原子操作和内存序假设;
  • CPU 对齐要求;
  • 原生扩展;
  • 加密库和 SIMD 指令;
  • 文件系统行为;
  • 外部数据库或系统库 ABI;
  • 依赖 /proc、设备文件或特定内核能力的程序。

多架构发布解决的是镜像分发和 CPU 架构适配问题,不是所有操作系统、内核和硬件差异的统一抽象。


十七、一个可审计的发布流程

下面是一套从构建到验证的完整流程。

第一步:准备 Dockerfile 和基础镜像

确保每个 FROM 镜像都支持目标平台:

docker buildx imagetools inspect golang:1.23-alpine
docker buildx imagetools inspect alpine:3.20

第二步:准备构建器

docker buildx create \
  --name multi-builder \
  --driver docker-container \
  --use

docker buildx inspect --bootstrap

如果使用 Linux 主机加 QEMU:

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

第三步:执行多平台构建并推送

docker buildx build \
  --builder multi-builder \
  --platform linux/amd64,linux/arm64 \
  --tag registry.example.com/demo/app:1.0 \
  --tag registry.example.com/demo/app:stable \
  --push \
  .

同一次构建可以推送多个 Tag,但这些 Tag 都是可变引用。需要不可变发布记录时,应保存最终 index digest。

第四步:检查远程索引

docker buildx imagetools inspect \
  registry.example.com/demo/app:1.0

确认:

  • 索引存在;
  • 目标平台齐全;
  • 每个平台都有独立子镜像 digest;
  • 没有意外出现不支持的平台。

第五步:在目标平台运行

docker run --rm \
  --platform linux/amd64 \
  registry.example.com/demo/app:1.0

docker run --rm \
  --platform linux/arm64 \
  registry.example.com/demo/app:1.0

如果是在 amd64 主机上测试 ARM64,这一步包含 QEMU 模拟;如果要验证真实 ARM64 行为,应在 ARM64 主机执行。

第六步:记录索引 digest

发布系统应保存类似记录:

image: registry.example.com/demo/app
tag: 1.0
index digest: sha256:...
platforms:
  - linux/amd64
  - linux/arm64

部署时可以使用:

docker pull registry.example.com/demo/app@sha256:<index-digest>

这样 Tag 即使后来被重新推送,部署对象仍然由 digest 唯一确定。


十八、几个必须避免的概念混淆

混淆一:Buildx 等于 QEMU

不等于。

Buildx:管理并调用 BuildKit
BuildKit:执行构建图并产出镜像
QEMU:模拟不同 CPU 架构的用户态指令

没有 QEMU,Buildx 仍然可以通过原生节点或交叉编译构建多架构镜像。

混淆二:多平台 Tag 等于一个通用文件系统

不等于。

多平台 Tag 通常指向一个索引,索引再指向多个平台镜像。每个平台可能拥有不同的层、配置和二进制。

混淆三:--platform 只能影响最终运行时

不完全正确。

在构建命令中:

--platform linux/amd64,linux/arm64

它影响构建目标集合。

在 Dockerfile 中:

FROM --platform=$BUILDPLATFORM ...

它影响某个阶段使用哪一个平台的基础镜像。

在 Compose 中:

platform: linux/amd64

它影响服务运行时选择的平台,也可能影响该服务的构建语义。

必须结合命令位置和配置上下文理解 platform

混淆四:构建成功证明跨平台发布成功

不一定。

还需要验证:

  1. 远程 Tag 是否是索引;
  2. 索引是否包含所有目标平台;
  3. 子镜像中的二进制是否匹配;
  4. 目标节点是否能够拉取并启动;
  5. 应用的架构相关测试是否通过。

多架构发布的核心链路可以归纳为:

目标平台集合
    -> BuildKit 分平台构建
    -> 原生执行、QEMU 模拟或交叉编译
    -> 每个平台生成独立镜像
    -> Manifest List / OCI Index 汇总
    -> Registry 保存 Tag 与 digest
    -> 客户端按平台选择子镜像

只要在每一层区分了“构建平台”“目标平台”“运行平台”,并且把 Tag、索引 digest 和平台子镜像 digest 分开验证,多架构构建就不再是一个依赖工具魔法的命令,而是一条可以检查、诊断和恢复的镜像发布流程。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。