Docker 基础体系 · 第 7/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 多架构构建:Buildx、QEMU、Manifest 与跨平台发布
“多架构镜像”不是把一个二进制文件同时标记成多个架构,而是为不同目标平台分别构建镜像,并让镜像仓库通过一个统一入口选择正确的镜像变体。
例如,linux/amd64 与 linux/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/amd64、linux/arm64 等平台;Windows 容器使用不同的基础镜像、内核和运行时约束,不能简单套用本文结论。
一、先建立平台模型:OS、架构与变体
Docker 使用平台标识描述镜像适用的运行环境,常见形式为:
os/architecture[/variant]
例如:
linux/amd64
linux/arm64
linux/arm/v7
linux/arm/v6
其中:
os表示操作系统,例如linux;architecture表示 CPU 架构,例如amd64、arm64、arm;variant表示同一架构下的变体,例如 ARMv7 的v7。
在 OCI Image Specification 中,平台信息还可以包含 os.version 和 os.features。实际 Docker 平台选择通常最关心操作系统、架构和架构变体。
可以把一个目标平台抽象为:
一个镜像变体只有在其平台属性与运行节点兼容时,才适合作为该节点的运行镜像。例如:
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 现代镜像构建系统。它负责:
- 解析 Dockerfile;
- 处理构建上下文;
- 执行每个构建阶段;
- 管理构建缓存;
- 处理 Secret、SSH 等构建会话;
- 生成镜像配置、文件层和最终镜像;
- 将结果输出到本地镜像存储、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
各命令的作用是:
create创建名为multi-builder的构建器;--driver docker-container让 BuildKit 运行在专用容器中;--use将当前 CLI 上下文切换到该构建器;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
可能原因有:
- 目标架构二进制被送到了没有对应 QEMU 的节点;
binfmt_misc没有配置;- 镜像平台声明与实际二进制不一致;
- 脚本的解释器不存在;
- 脚本使用了错误的换行格式,例如从 Windows 工作区复制了 CRLF;
- 程序使用了模拟器不支持或表现不同的指令。
因此,“安装了 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-context 和 arm64-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
然后通过 GOOS 和 GOARCH 产生对应目标二进制。这样做的关键原因是:编译器不需要以目标架构执行,减少了对 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 单个平台镜像的结构
一个容器镜像通常至少包含:
- 配置对象;
- 一个或多个文件系统层;
- 镜像清单(manifest)。
清单记录配置摘要和层摘要。例如,抽象后可以表示为:
{
"schemaVersion": 2,
"config": {
"digest": "sha256:config..."
},
"layers": [
{ "digest": "sha256:layer-1..." },
{ "digest": "sha256:layer-2..." }
]
}
摘要是内容寻址标识:
同样内容会产生同样摘要;内容变化会导致摘要变化。
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[客户端按平台选择]
关键状态变化如下:
- Buildx 接收
--platform linux/amd64,linux/arm64; - BuildKit 为每个目标平台建立独立的构建结果;
- 各平台阶段产生各自的配置和文件层;
- 每个平台生成一个子镜像 Manifest;
- BuildKit 生成上层 Image Index 或 Manifest List;
- Registry 将索引和子镜像对象保存起来;
docker pull请求 Tag;- Registry 返回索引;
- Docker Engine 使用自身平台选择子镜像;
- 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。platform 与 build.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 运行失败或构建极慢
如果出现非法指令、测试进程崩溃或构建时间明显增加,应区分三类问题:
- 模拟器或 binfmt 配置错误;
- 目标程序使用了模拟环境不支持的指令或行为;
- 构建本身需要大量目标架构执行。
诊断方式包括:
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/amd64 和 linux/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。
混淆四:构建成功证明跨平台发布成功
不一定。
还需要验证:
- 远程 Tag 是否是索引;
- 索引是否包含所有目标平台;
- 子镜像中的二进制是否匹配;
- 目标节点是否能够拉取并启动;
- 应用的架构相关测试是否通过。
多架构发布的核心链路可以归纳为:
目标平台集合
-> BuildKit 分平台构建
-> 原生执行、QEMU 模拟或交叉编译
-> 每个平台生成独立镜像
-> Manifest List / OCI Index 汇总
-> Registry 保存 Tag 与 digest
-> 客户端按平台选择子镜像
只要在每一层区分了“构建平台”“目标平台”“运行平台”,并且把 Tag、索引 digest 和平台子镜像 digest 分开验证,多架构构建就不再是一个依赖工具魔法的命令,而是一条可以检查、诊断和恢复的镜像发布流程。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 多阶段构建:最小运行时、依赖固定、调试层和体积优化
- 下一篇:Docker 网络完整指南:Bridge、端口映射、DNS、Overlay 和排障
- 延伸:Dockerfile 与 BuildKit:构建上下文、缓存挂载、Secret 和可重复构建
- 延伸:Docker Registry 与镜像分发:Tag、Digest、认证、缓存和清理
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论