容器镜像(3):多架构镜像构建
一、什么是多架构镜像
1.1 OCI Image Index
上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起:
$ docker manifest inspect golang:1.22-alpine
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"digest": "sha256:3b4d6fe...",
"platform": { "architecture": "amd64", "os": "linux" }
},
{
"digest": "sha256:9c2a1fe...",
"platform": { "architecture": "arm64", "os": "linux", "variant": "v8" }
},
{
"digest": "sha256:f12b3cd...",
"platform": { "architecture": "arm", "os": "linux", "variant": "v7" }
},
{
"digest": "sha256:a4c8d91...",
"platform": { "architecture": "s390x", "os": "linux" }
}
]
}
层次关系如下:
Image Index
├── linux/amd64 → Manifest A → Config + Layer blobs(x86 二进制)
├── linux/arm64 → Manifest B → Config + Layer blobs(ARM64 二进制)
├── linux/arm/v7 → Manifest C → Config + Layer blobs(ARMv7 二进制)
└── linux/s390x → Manifest D → Config + Layer blobs(IBM Z 二进制)
docker pull golang:1.22-alpine 时,客户端在请求头里携带当前平台信息,Registry 返回 Image Index 后,客户端找到匹配条目,再用对应的 digest 拉取实际的单平台 Manifest 和 blobs,整个过程对用户透明。
1.2 构建多架构镜像的前提
编写一个同时支持 linux/amd64 和 linux/arm64 的 Dockerfile 时,FROM 的基础镜像本身也必须支持这两个架构——构建工具在打包 amd64 版本时会自动拉取对应变体,打包 arm64 时同理。若基础镜像缺少某个架构,构建立即报错:
ERROR: failed to solve: golang:1.22-alpine: no match for platform linux/arm64
在可以直连 Docker Hub 的环境里这不是问题,官方镜像基本都有完整的多架构支持。麻烦在于企业内网——构建集群往往只能访问私有 Registry,基础镜像需要先从公共 Registry 搬运进来。
二、向私有 Registry 同步多架构镜像
2.1 docker pull + push 会丢失 Image Index
最直觉的搬运方式有一个隐蔽的陷阱:
# ❌ 错误做法
docker pull golang:1.22-alpine # 只拉取了当前机器架构的单平台 Manifest
docker tag golang:1.22-alpine registry.internal/golang:1.22-alpine
docker push registry.internal/golang:1.22-alpine # Image Index 已丢失
推完之后检查,会发现 mediaType 变成了单平台 Manifest,多架构信息完全丢失,arm64 构建必然失败。
2.2 用 skopeo 正确复制
skopeo 不依赖 Docker daemon,直接操作 Registry API,天然支持保留 Image Index。
# 复制全部架构,--all 保留完整 Image Index
skopeo copy --all \
docker://docker.io/library/golang:1.22-alpine \
docker://registry.internal/golang:1.22-alpine
# 需要目标 Registry 认证时,加 --dest-creds
skopeo copy --all \
--dest-creds user:password \
docker://docker.io/library/golang:1.22-alpine \
docker://registry.internal/golang:1.22-alpine
存储空间有限时,可以只同步所需架构,分两步完成:
# 第一步:分别复制单平台镜像
skopeo copy --override-arch amd64 --override-os linux \
docker://docker.io/library/golang:1.22-alpine \
docker://registry.internal/golang:1.22-alpine-amd64
skopeo copy --override-arch arm64 --override-os linux \
docker://docker.io/library/golang:1.22-alpine \
docker://registry.internal/golang:1.22-alpine-arm64
# 第二步:重新组装 Image Index
docker buildx imagetools create \
--tag registry.internal/golang:1.22-alpine \
registry.internal/golang:1.22-alpine-amd64 \
registry.internal/golang:1.22-alpine-arm64
2.3 用 crane 复制
crane 支持在一条命令里直接过滤架构,无需两步:
# 复制全部架构
crane copy --platform all \
golang:1.22-alpine \
registry.internal/golang:1.22-alpine
# 只复制 amd64 和 arm64
crane copy --platform linux/amd64,linux/arm64 \
golang:1.22-alpine \
registry.internal/golang:1.22-alpine
# crane 需要单独登录目标 Registry
crane auth login registry.internal -u user -p password
同步完成后,用下面的命令确认目标架构就位,再启动构建:
$ docker buildx imagetools inspect registry.internal/golang:1.22-alpine \
--format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}}{{"\n"}}{{end}}'
linux/amd64
linux/arm64
三、离线传输:将多架构镜像搬运到内网机器
有时目标机器既无法访问外网,也无法访问源 Registry(例如完全隔离的内网或现场环境),只能通过物理介质(U 盘、安全摆渡机)或单次 SCP 传输来搬运镜像。这时需要先把镜像存为文件,再导入目标端的 Registry 或 Docker。
3.1 docker save / load 仍然不适用于多架构
docker save 与 docker pull 有同样的问题,只导出当前平台的单架构镜像:
# ❌ docker save 只导出当前平台的单架构镜像
docker save registry.internal/myapp:v1.0 -o myapp.tar
docker load -i myapp.tar # 目标机器上只有 amd64,arm64 缺失
3.2 用 skopeo 保存为 OCI 归档文件(推荐)
skopeo 的 oci-archive: 传输协议会将镜像打包成一个单一的 tar 文件,同时完整保留 Image Index:
# ── 在源端(有访问 Registry 权限的机器)──────────────────────────
# 将多架构镜像导出为单个 tar 文件
skopeo copy --all \
docker://registry.internal/myapp:v1.0 \
oci-archive:/tmp/myapp.tar
# 传输到目标机器(SCP / rsync / U 盘均可)
scp /tmp/myapp.tar user@target-host:/opt/images/
# ── 在目标端 ──────────────────────────────────────────────────────
# 方式一:直接推送到目标 Registry(推荐)
skopeo copy --all \
oci-archive:/opt/images/myapp.tar \
docker://target-registry/myapp:v1.0
# 方式二:加载到本地 Docker(只会加载当前平台的那个变体)
# 如果目标机器只需要运行,不需要重新推送,可用此方式
docker load -i /opt/images/myapp.tar
验证推入目标 Registry 后 Image Index 是否完整:
$ skopeo inspect --raw docker://target-registry/myapp:v1.0 \
| python3 -c "import sys,json; idx=json.load(sys.stdin); \
[print(m['platform']['architecture']) for m in idx['manifests']]"
amd64
arm64
3.3 用 skopeo 保存为 OCI 目录格式
oci: 传输协议将镜像保存为一个目录结构(符合 OCI Image Layout 规范),更便于后续检查各个 blob 文件:
# 保存为目录
skopeo copy --all \
docker://registry.internal/myapp:v1.0 \
oci:/opt/images/myapp-oci
# 打包传输
tar czf myapp-oci.tar.gz -C /opt/images myapp-oci/
scp myapp-oci.tar.gz user@target-host:/opt/
# 目标端解压后推送
tar xzf myapp-oci.tar.gz -C /opt/images
skopeo copy --all \
oci:/opt/images/myapp-oci \
docker://target-registry/myapp:v1.0
oci-archive: 与 oci: 的区别仅在于前者是单个 tar 文件(方便传输),后者是目录(方便检查内容)。
3.4 两端都能访问 Registry 时:直接跨 Registry 复制
如果源端和目标端通过跳板机或 VPN 能同时访问两个 Registry,无需落盘,skopeo 可以直接在两个 Registry 之间流式传输:
skopeo copy --all \
--src-creds src-user:src-pass \
--dest-creds dst-user:dst-pass \
docker://source-registry/myapp:v1.0 \
docker://target-registry/myapp:v1.0
这是最简洁的方式,避免了中间文件的生成,也是搭建内网镜像同步服务的常用做法。
四、示例项目
后续几节用同一个 Golang 项目演示多架构构建。项目只有两个文件。
main.go:运行时打印当前操作系统和 CPU 架构,便于验证拉取到的是否是正确平台的镜像:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("Hello! Running on %s/%s\n", runtime.GOOS, runtime.GOARCH)
}
Dockerfile:多阶段构建,编译阶段使用 golang 镜像,运行阶段只保留最终二进制:
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY main.go .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/app .
FROM alpine:3.21
COPY --from=build /out/app /usr/local/bin/app
ENTRYPOINT ["app"]
CGO_ENABLED=0 关闭 cgo,产出纯静态二进制,不依赖 libc。-trimpath -ldflags="-s -w" 去掉调试信息,减小体积。
五、多架构构建的本质:各架构分别构建,最后合并
在介绍 buildx 的 QEMU 模拟方案之前,先看一下多架构构建的本质做法:在对应架构的原生机器上分别构建,得到两个单平台镜像,最后用 imagetools create 或 podman manifest create 把它们合并成一个 Image Index。
这是多架构镜像最直白的构建路径,也完整展示了 Image Index 的本质——它本身只是一张"指针表",把若干个独立的单平台镜像关联在一起,每个镜像可以完全独立构建、独立部署。
5.1 在各自架构的机器上分别构建
# ── 在 amd64 机器上执行 ────────────────────────────────────────
$ uname -m
x86_64
$ docker build \
--platform linux/amd64 \
--tag registry.internal/myapp:v1.0-amd64 \
--push \
.
# ── 在 arm64 机器上执行(SSH 到 ARM 服务器或树莓派)──────────────
$ uname -m
aarch64
$ docker build \
--platform linux/arm64 \
--tag registry.internal/myapp:v1.0-arm64 \
--push \
.
此时 Registry 里有两个独立的单平台镜像,各自完全可用:
# amd64 机器上运行 amd64 版本
$ docker run --rm registry.internal/myapp:v1.0-amd64
Hello! Running on linux/amd64
# arm64 机器上运行 arm64 版本
$ docker run --rm registry.internal/myapp:v1.0-arm64
Hello! Running on linux/arm64
5.2 合并为多架构镜像
# 用 imagetools create 将两个单平台镜像合并为一个 Image Index
$ docker buildx imagetools create \
--tag registry.internal/myapp:v1.0 \
registry.internal/myapp:v1.0-amd64 \
registry.internal/myapp:v1.0-arm64
# 验证合并结果
$ docker buildx imagetools inspect registry.internal/myapp:v1.0
Name: registry.internal/myapp:v1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:c7a2b31...
Manifests:
Name: registry.internal/myapp:v1.0@sha256:a1b2c3...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/amd64
Name: registry.internal/myapp:v1.0@sha256:d4e5f6...
MediaType: application/vnd.oci.image.manifest.v1+json
Platform: linux/arm64
imagetools create 做的事非常简单:读取两个单平台镜像各自的 Manifest digest,生成一个新的 Image Index JSON,把它们的 digest + platform 条目填进去,推送到 Registry。两个单平台镜像的 blob 数据原封不动,只是新建了一个索引来引用它们。
用 Podman 也是完全相同的思路:
# 各架构分别构建并推送(在对应机器上执行)
podman build -t registry.internal/myapp:v1.0-amd64 --push . # amd64 机器
podman build -t registry.internal/myapp:v1.0-arm64 --push . # arm64 机器
# 合并
podman manifest create registry.internal/myapp:v1.0 \
registry.internal/myapp:v1.0-amd64 \
registry.internal/myapp:v1.0-arm64
podman manifest push registry.internal/myapp:v1.0
这种分步做法在 CI 系统里也非常常见:amd64 和 arm64 构建任务并行跑在各自的 runner 上,都完成后触发一个合并任务生成最终的 Image Index。
六、用 Docker buildx 在单机构建(QEMU 方案)
如果没有 arm64 原生机器,可以在单台 amd64 机器上通过 QEMU 模拟执行异架构二进制,一条命令完成两个平台的构建。代价是 arm64 编译步骤要通过软件模拟执行,速度明显慢于原生。
6.1 安装 QEMU 并注册 binfmt
sudo apt install -y qemu-user-static binfmt-support
# 用 multiarch 官方镜像一键注册所有架构的 binfmt_misc
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
# 验证 arm64 已可模拟
ls /proc/sys/fs/binfmt_misc/ | grep qemu-aarch64
qemu-aarch64
6.2 创建 buildx builder
docker buildx create \
--name multiarch-builder \
--driver docker-container \
--use
# 启动并确认支持 arm64
docker buildx inspect --bootstrap | grep -A2 Platforms
Platforms: linux/amd64, linux/arm64, linux/arm/v7, ...
6.3 构建并推送
docker buildx build \
--platform linux/amd64,linux/arm64 \
--tag registry.internal/myapp:v1.0 \
--push \
.
构建过程中两个平台并行进行,arm64 步骤通过 QEMU 模拟,耗时约为 amd64 的 2–4 倍:
[+] Building 42.3s
=> [linux/amd64 build 3/3] RUN CGO_ENABLED=0 go build ... 5.3s
=> [linux/arm64 build 3/3] RUN CGO_ENABLED=0 go build ... 14.7s ← QEMU 模拟
=> pushing manifest list 1.1s
6.4 验证
# 查看 Image Index
$ docker buildx imagetools inspect registry.internal/myapp:v1.0
...
Platform: linux/amd64
Platform: linux/arm64
# 当前机器(amd64)正常运行
$ docker run --rm registry.internal/myapp:v1.0
Hello! Running on linux/amd64
# 强制拉取 arm64 变体,通过 QEMU 运行
$ docker run --rm --platform linux/arm64 registry.internal/myapp:v1.0
Hello! Running on linux/arm64
七、用 Podman 构建多架构镜像
Podman 4.x 引入了 build --manifest 参数,可以在构建时直接指定目标 manifest list:
# 一条命令构建两个平台并生成本地 manifest list
podman build \
--platform linux/amd64,linux/arm64 \
--manifest myapp:v1.0 \
.
# 推送 manifest list 及所有平台镜像
podman login registry.internal
podman manifest push myapp:v1.0 registry.internal/myapp:v1.0
想对每个平台做差异化控制时,也可以走显式分步流程(与第五节原生分步构建的逻辑完全一致):
podman build --platform linux/amd64 -t registry.internal/myapp:v1.0-amd64 .
podman build --platform linux/arm64 -t registry.internal/myapp:v1.0-arm64 .
podman push registry.internal/myapp:v1.0-amd64
podman push registry.internal/myapp:v1.0-arm64
podman manifest create registry.internal/myapp:v1.0 \
registry.internal/myapp:v1.0-amd64 \
registry.internal/myapp:v1.0-arm64
podman manifest push registry.internal/myapp:v1.0
八、各方案横向对比
8.1 构建方案对比
| 方案 | 需要条件 | 构建速度 | 适合场景 |
|---|---|---|---|
| 各架构原生分步构建 + 合并 | 各架构的原生机器 | 最快(原生编译) | 大型项目、CI 多 runner |
| Docker buildx + QEMU | 单台 x86 机器 + QEMU | arm64 慢 2–4 倍 | 个人开发、小项目、快速验证 |
Podman --manifest |
单台机器 + QEMU | 同上 | 无 daemon 环境、rootless CI |
8.2 镜像传输/同步方案对比
| 方案 | 适用场景 | 是否保留 Image Index |
|---|---|---|
docker pull + push |
❌ 不适用 | ✗ 丢失 |
skopeo copy --all(Registry 间) |
两端可同时访问 Registry | ✓ |
skopeo copy + oci-archive: |
离线传输、物理介质 | ✓ |
crane copy --platform |
两端可同时访问 Registry | ✓ |
九、小结
多架构镜像的本质是 OCI Image Index:一个把多个单平台 Manifest 聚合起来的索引,客户端拉取时按平台自动匹配,Image Index 本身不包含任何二进制数据。
理解了这一点,构建和传输的正确路径就都清晰了:
构建的本质是分别为每个架构产出一个单平台镜像,再用 imagetools create 或 manifest create 生成一个指向它们的 Index。QEMU 方案只是在没有异架构原生机器时的便捷替代——通过软件模拟让单台机器完成这个过程。
传输时,docker save/load 和 docker pull/push 均无法保留 Image Index,必须使用能感知 OCI Index 结构的工具。skopeo copy --all 配合 oci-archive: 传输协议是最可靠的离线搬运方式;如果两端有网络联通,skopeo 或 crane 可以直接做 Registry 间的流式复制,更为简洁。
下一篇,我们将介绍一些日常操作多架构镜像时实用的辅助工具:skopeo、crane、regctl,以及镜像签名工具 cosign。