目录

Docker 多平台镜像教程:使用 docker buildx 构建 amd64 和 arm64 镜像

问题:exec format error

在搭载 Apple Silicon(M 系列芯片)的 Mac 上构建镜像,并将其推送到普通云主机运行,容器一启动就退出:

exec /app: exec format error

原因是 CPU 架构不同。Apple Silicon Mac 默认构建 arm64 镜像,而大多数云主机使用 amd64(x86_64)。程序编译成一种 CPU 的机器码后,另一种 CPU 无法识别,因此会出现此错误。后文会实际重现。

常见的平台

平台常见设备
linux/amd64大多数云主机、Intel / AMD 电脑
linux/arm64Apple Silicon Mac、AWS Graviton、Google Axion、树莓派 4/5(64 位系统)
linux/arm/v7旧款树莓派(32 位系统)

什么是多平台镜像

官方镜像(例如 nginx、node)是多平台镜像:同一个名称和 tag 下存放着多个架构的版本。查看 nginx:1.30-alpine 支持哪些平台:

$ docker buildx imagetools inspect nginx:1.30-alpine | grep "Platform:" | grep -v unknown
  Platform:    linux/amd64
  Platform:    linux/arm/v6
  Platform:    linux/arm/v7
  Platform:    linux/arm64/v8
  Platform:    linux/386
  Platform:    linux/ppc64le
  Platform:    linux/riscv64
  Platform:    linux/s390x

同一个 nginx:1.30-alpine 包含 8 种架构。执行 docker pull 时,Docker 会自动选择与当前主机匹配的版本,因此同一条 docker run nginx:1.30-alpine 命令在 Mac 和云主机上都能运行。我们也可以让自己的镜像支持这种用法。

准备示例:Go Web 服务

用一个会显示自身运行架构的 Go 程序进行演示。文件夹 goapp:

goapp/
├── go.mod
├── main.go
└── Dockerfile

go.mod:

module example.com/server

go 1.27

main.go:

package main

import (
	"fmt"
	"net/http"
	"runtime"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from %s/%s\n", runtime.GOOS, runtime.GOARCH)
	})
	fmt.Printf("Listening on :8080 (%s/%s)\n", runtime.GOOS, runtime.GOARCH)
	http.ListenAndServe(":8080", nil)
}

Dockerfile:

FROM --platform=$BUILDPLATFORM golang:1.27 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o /out/app .

FROM gcr.io/distroless/static-debian13
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

这是一个用于交叉编译的 Dockerfile,关键在于 BuildKit 自动提供的几个参数:

参数含义本例中的值
BUILDPLATFORM执行构建的机器平台linux/amd64
TARGETPLATFORM要生成的镜像平台linux/amd64 或 linux/arm64
TARGETOS / TARGETARCH目标平台的操作系统和架构linux / amd64 或 arm64
  • FROM --platform=$BUILDPLATFORM golang:1.27:编译阶段始终在构建机器自身的架构上运行,速度与原生运行相同。
  • GOOS=$TARGETOS GOARCH=$TARGETARCH:让 Go 编译器直接生成目标架构的可执行文件。
  • 最后一个阶段只执行 COPY,不运行任何程序,因此不需要模拟目标 CPU。

使用 buildx 一次构建两个平台

docker buildx 是使用 BuildKit 的构建命令,Docker Desktop 和 Linux 的 docker-buildx-plugin 都已内置。先查看当前的 builder:

$ docker buildx ls
NAME/NODE     DRIVER/ENDPOINT   STATUS    BUILDKIT   PLATFORMS
default*      docker
 \_ default    \_ default       running   v0.34.0    linux/amd64 (+3)

Docker 29 默认使用 containerd 镜像存储,默认的 docker builder 就可以构建多平台镜像并将其保存在本机。使用 --platform 指定多个平台:

$ docker buildx build --platform linux/amd64,linux/arm64 -t goapp:1.0 .

[+] Building 106.9s (16/16) FINISHED                                                                                                   docker:default
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 304B                                                                                                             0.0s
 => [linux/arm64 internal] load metadata for gcr.io/distroless/static-debian13:latest                                                            2.0s
 => [linux/amd64 internal] load metadata for gcr.io/distroless/static-debian13:latest                                                            1.8s
 => [linux/amd64 internal] load metadata for docker.io/library/golang:1.27                                                                       1.5s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                  0.0s
 => [linux/amd64 build 1/4] FROM docker.io/library/golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602          19.3s
 => => resolve docker.io/library/golang:1.27@sha256:e432b43af23a9328d56a7c499be0476810aa344acbcf65fc7c455d4ff5a40602                             0.0s
 => => sha256:a6132307e9f8de84712b13dc48449cc6a8a80bc26e35b5c9dd04a6df2b23e85e 102.41MB / 102.41MB                                               3.3s
 => => sha256:b2b6413ae8f393d11d32262c8dc6bd053dbaea3d074f95edd5605d3eb116e3c2 71.15MB / 71.15MB                                                 2.5s
 => => sha256:801401a9a8ba22fe48ef647d8eed0d6d8adc41baabf2fae9057ff38b2622f35a 127B / 127B                                                       0.3s
 => => sha256:4dc3950125ebc9ed195e1101fc6c02e2fce113ba0f53bfb05b2cdf58747df9af 67.81MB / 67.81MB                                                 2.6s
 => => sha256:62fe2e747f69f2f414c7e9eb1553155cf0a658398f0462557f458821dce2b29d 25.64MB / 25.64MB                                                 1.5s
 => => sha256:4846f29007305297af653c57bf9b60ba8f676a38695c68d98460063442581671 49.39MB / 49.39MB                                                 1.4s
 => => extracting sha256:4846f29007305297af653c57bf9b60ba8f676a38695c68d98460063442581671                                                        2.7s
 => => extracting sha256:62fe2e747f69f2f414c7e9eb1553155cf0a658398f0462557f458821dce2b29d                                                        0.9s
 => => extracting sha256:4dc3950125ebc9ed195e1101fc6c02e2fce113ba0f53bfb05b2cdf58747df9af                                                        3.1s
 => => extracting sha256:a6132307e9f8de84712b13dc48449cc6a8a80bc26e35b5c9dd04a6df2b23e85e                                                        4.3s
 => => extracting sha256:b2b6413ae8f393d11d32262c8dc6bd053dbaea3d074f95edd5605d3eb116e3c2                                                        4.9s
 => => extracting sha256:801401a9a8ba22fe48ef647d8eed0d6d8adc41baabf2fae9057ff38b2622f35a                                                        0.0s
 => => extracting sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1                                                        0.0s
 => [linux/amd64 stage-1 1/2] FROM gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0f4e97a35cebf15ef4ea08f8a4c6d2ee5f75e4a  4.3s
 => => resolve gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0f4e97a35cebf15ef4ea08f8a4c6d2ee5f75e4aa6a0                  0.0s
 => => sha256:c4bc6f35ff5e25aa1afe7a148a0b6f10a760f54e298f86a4a4d44b91f1afbfab 143.34kB / 143.34kB                                               1.0s
 => => sha256:2cc7ee286bf3a9e6af5f71756d7fc8e22e23ce65fec9047d774511ffcef79fa8 124.53kB / 124.53kB                                               0.9s
 => => extracting sha256:2cc7ee286bf3a9e6af5f71756d7fc8e22e23ce65fec9047d774511ffcef79fa8                                                        0.1s
 => => extracting sha256:c172f21841dff4c8cf45cde46589c1c2616cefe7e819965e92e6d3475c428aa0                                                        0.1s
 => => extracting sha256:218cf840d0d95a86231eb9d33d26ac8a520a4299c6096cb70a0c2fb2a87c9e0a                                                        0.3s
 => => extracting sha256:f6069939f718601b38d25ec5ed5631ed047e4da24acec2e252f8f29d251be718                                                        0.2s
 => => extracting sha256:d6b1b89eccacc15c2420b2776d72c1dae334a00805ed9af54bf2f71e4d536f28                                                        0.0s
 => => extracting sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a                                                        0.0s
 => => extracting sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212                                                        0.0s
 => => extracting sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4                                                        0.0s
 => => extracting sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296                                                        0.0s
 => => extracting sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112                                                        0.0s
 => => extracting sha256:b839dfae01f66e15c6a8b63520557ed315bdfe036342fa7a0c537259f10d7a9a                                                        0.0s
 => => extracting sha256:ebddc55facdc6b1f7e0f30816a5fc7cc62f38abdf76c0a8b0a0ce52085754795                                                        0.1s
 => => extracting sha256:c4bc6f35ff5e25aa1afe7a148a0b6f10a760f54e298f86a4a4d44b91f1afbfab                                                        0.1s
 => [linux/arm64 stage-1 1/2] FROM gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0f4e97a35cebf15ef4ea08f8a4c6d2ee5f75e4a  4.4s
 => => resolve gcr.io/distroless/static-debian13:latest@sha256:58133991db06659feaabe0f4e97a35cebf15ef4ea08f8a4c6d2ee5f75e4aa6a0                  0.0s
 => => sha256:b839dfae01f66e15c6a8b63520557ed315bdfe036342fa7a0c537259f10d7a9a 351B / 351B                                                       0.3s
 => => sha256:ebddc55facdc6b1f7e0f30816a5fc7cc62f38abdf76c0a8b0a0ce52085754795 311B / 311B                                                       0.3s
 => => sha256:6ed1ef0cb1159b9c93a24ceb88b987c6b3a8adff7160940c7c5e6bfc001f1766 143.34kB / 143.34kB                                               1.7s
 => => sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112 80B / 80B                                                         0.8s
 => => sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296 162B / 162B                                                       0.5s
 => => sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4 123B / 123B                                                       0.3s
 => => sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212 188B / 188B                                                       0.3s
 => => sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a 67B / 67B                                                         0.4s
 => => sha256:d6b1b89eccacc15c2420b2776d72c1dae334a00805ed9af54bf2f71e4d536f28 32.09kB / 32.09kB                                                 0.7s
 => => sha256:f6069939f718601b38d25ec5ed5631ed047e4da24acec2e252f8f29d251be718 254.67kB / 254.67kB                                               1.1s
 => => sha256:218cf840d0d95a86231eb9d33d26ac8a520a4299c6096cb70a0c2fb2a87c9e0a 288.06kB / 288.06kB                                               1.8s
 => => sha256:c172f21841dff4c8cf45cde46589c1c2616cefe7e819965e92e6d3475c428aa0 12.68kB / 12.68kB                                                 0.3s
 => => sha256:401d177fd1e8a1e0d7a48139788d026e9a2e919ccb122bedd110ac7cf351f53e 124.50kB / 124.50kB                                               1.1s
 => => extracting sha256:401d177fd1e8a1e0d7a48139788d026e9a2e919ccb122bedd110ac7cf351f53e                                                        0.1s
 => => extracting sha256:c172f21841dff4c8cf45cde46589c1c2616cefe7e819965e92e6d3475c428aa0                                                        0.0s
 => => extracting sha256:218cf840d0d95a86231eb9d33d26ac8a520a4299c6096cb70a0c2fb2a87c9e0a                                                        0.2s
 => => extracting sha256:f6069939f718601b38d25ec5ed5631ed047e4da24acec2e252f8f29d251be718                                                        0.3s
 => => extracting sha256:d6b1b89eccacc15c2420b2776d72c1dae334a00805ed9af54bf2f71e4d536f28                                                        0.0s
 => => extracting sha256:2780920e5dbfbe103d03a583ed75345306e572ec5a48cb10361f046767d9f29a                                                        0.0s
 => => extracting sha256:7c12895b777bcaa8ccae0605b4de635b68fc32d60fa08f421dc3818bf55ee212                                                        0.0s
 => => extracting sha256:3214acf345c0cc6bbdb56b698a41ccdefc624a09d6beb0d38b5de0b2303ecaf4                                                        0.0s
 => => extracting sha256:52630fc75a18675c530ed9eba5f55eca09b03e91bd5bc15307918bbc1a7e7296                                                        0.0s
 => => extracting sha256:dd64bf2dd177757451a98fcdc999a339c35dee5d9872d8f4dc69c8f3c4dd0112                                                        0.0s
 => => extracting sha256:b839dfae01f66e15c6a8b63520557ed315bdfe036342fa7a0c537259f10d7a9a                                                        0.0s
 => => extracting sha256:ebddc55facdc6b1f7e0f30816a5fc7cc62f38abdf76c0a8b0a0ce52085754795                                                        0.0s
 => => extracting sha256:6ed1ef0cb1159b9c93a24ceb88b987c6b3a8adff7160940c7c5e6bfc001f1766                                                        0.1s
 => [internal] load build context                                                                                                                0.0s
 => => transferring context: 735B                                                                                                                0.0s
 => [linux/amd64 build 2/4] WORKDIR /src                                                                                                         0.1s
 => [linux/amd64 build 3/4] COPY . .                                                                                                             0.0s
 => [linux/amd64 build 4/4] RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o /out/app .                                                    83.3s
 => [linux/amd64->arm64 build 4/4] RUN GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o /out/app .                                             83.8s
 => [linux/amd64 stage-1 2/2] COPY --from=build /out/app /app                                                                                    0.0s
 => [linux/arm64 stage-1 2/2] COPY --from=build /out/app /app                                                                                    0.1s
 => exporting to image                                                                                                                           1.4s
 => => exporting layers                                                                                                                          0.9s
 => => exporting manifest sha256:bae3f4b45be6e6c37bbe5430c1fd68cf98974b82dd312b81fcf568abb321584c                                                0.0s
 => => exporting config sha256:795e98f14dcf24dbcc9a133515ee54505a800a4ebf3f2408d89441c3a166c468                                                  0.0s
 => => exporting attestation manifest sha256:379134f8a7c50a77e0bf19cbd9593a884daeac7eb2b7c852ccfa8f81ce7bdd34                                    0.0s
 => => exporting manifest sha256:d6cd4f43dd3d332129c7b6b9f6720c6da06e070125860d2ab0661e15d251133a                                                0.0s
 => => exporting config sha256:d61c8d18abcb388876ad82cb6009d9bd18754e6e277ac6822c697d407243594a                                                  0.0s
 => => exporting attestation manifest sha256:2f4085c9f5da45df6153b09141bf7db573806a93a2ed81c64c2ec75e49bfa8e6                                    0.0s
 => => exporting manifest list sha256:dc8d3f71a9f7adb0de32dd59651adc81dab5b78612cdc7eccc39a5f73608d37e                                           0.0s
 => => naming to docker.io/library/goapp:1.0                                                                                                     0.0s
 => => unpacking to docker.io/library/goapp:1.0                                                                                                  0.4s

观察构建过程:

  • 每个步骤前都标出了平台,例如 [linux/amd64 build 4/4]。
  • arm64 的编译步骤显示为 [linux/amd64->arm64 build 4/4]:在 amd64 机器上为 arm64 编译。这就是交叉编译。
  • 两个平台的 go build 并行执行。

使用 docker images --tree 查看结果(只有新版 Docker 才提供 --tree 选项):

$ docker images --tree goapp:1.0
IMAGE                ID             DISK USAGE   CONTENT SIZE   EXTRA
goapp:1.0            dc8d3f71a9f7       25.5MB         11.1MB
├─ linux/amd64       bae3f4b45be6       20.2MB         5.77MB
└─ linux/arm64       d6cd4f43dd3d       5.36MB         5.36MB

goapp:1.0 下有 linux/amd64 和 linux/arm64 两个版本。arm64 版本的 DISK USAGE 较小,因为本机不会运行它,所以没有将其解压。

在较旧的 Docker 版本中,或未启用 containerd 镜像存储时,默认 builder 无法保存多平台镜像。这时需要先创建一个使用 docker-container 驱动的 builder:docker buildx create --name multi --driver docker-container --use,并在构建时添加 --push,直接推送到 Registry。

实测:在不同架构上运行

在 amd64 主机上运行时,Docker 会自动选择 amd64 版本:

$ docker run -d --name goapp -p 8080:8080 goapp:1.0
2d1485d9ba48cf1b87983ff4d797699dde128295880f1fe965f24801da611ac4

$ curl -s http://localhost:8080/
Hello from linux/amd64

$ docker logs goapp
Listening on :8080 (linux/amd64)

$ docker rm -f goapp
goapp

程序报告自身的架构为 linux/amd64。

强制运行 arm64 版本,就会重现开头的错误:

$ docker run --rm --platform linux/arm64 goapp:1.0
exec /app: exec format error

$ docker run --rm --platform linux/arm64 alpine:3.23 uname -m
Unable to find image 'alpine:3.23' locally
3.23: Pulling from library/alpine
Digest: sha256:85fe1e81d6758c208f3e1eed4338a1997e19d4be002d4dd32d3100c9a8c010a0
Status: Image is up to date for alpine:3.23
exec /bin/uname: exec format error
  • 我们的 arm64 可执行文件在 amd64 主机上运行时出现 exec format error。
  • 官方的 alpine:3.23 也一样:Docker 下载了 arm64 版本,但主机 CPU 无法运行它。

使用 docker image inspect 确认镜像的架构;添加 --platform 可以查看指定平台的版本:

$ docker image inspect --format "{{.Os}}/{{.Architecture}}" goapp:1.0
linux/amd64

$ docker image inspect --platform linux/arm64 --format "{{.Os}}/{{.Architecture}}" goapp:1.0
linux/arm64

$ uname -m
x86_64

何时需要 QEMU 模拟器

上面的交叉编译之所以不需要模拟,是因为构建过程中没有运行任何目标平台的程序。如果 Dockerfile 在目标平台的镜像中执行命令(RUN),情况就不同了:

FROM alpine:3.23
RUN apk add --no-cache curl
$ docker buildx build --platform linux/arm64 -t arm-test .

[+] Building 0.5s (5/5) FINISHED                                                                                                       docker:default
 => [internal] load build definition from Dockerfile                                                                                             0.0s
 => => transferring dockerfile: 82B                                                                                                              0.0s
 => [internal] load metadata for docker.io/library/alpine:3.23                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                  0.0s
 => [1/2] FROM docker.io/library/alpine:3.23@sha256:85fe1e81d6758c208f3e1eed4338a1997e19d4be002d4dd32d3100c9a8c010a0                             0.1s
 => => resolve docker.io/library/alpine:3.23@sha256:85fe1e81d6758c208f3e1eed4338a1997e19d4be002d4dd32d3100c9a8c010a0                             0.0s
 => => extracting sha256:ace1621be7ff15b54252f68393ac33181df7f3e095e36a5d9a9892031b357d31                                                        0.1s
 => ERROR [2/2] RUN apk add --no-cache curl                                                                                                      0.3s
------
 > [2/2] RUN apk add --no-cache curl:
0.250 exec /bin/sh: exec format error
------
Dockerfile:2
--------------------
   1 |     FROM alpine:3.23
   2 | >>> RUN apk add --no-cache curl
   3 |
--------------------
ERROR: failed to build: failed to solve: process "/bin/sh -c apk add --no-cache curl" did not complete successfully: exit code: 255

RUN apk add 需要在 arm64 Alpine 中运行 /bin/sh,amd64 主机无法运行它,同样会出现 exec format error。此时需要 QEMU 模拟器,让 amd64 主机能够运行 arm64 程序:

  • Docker Desktop(Windows、macOS):已内置 QEMU,无需设置,直接使用 --platform 构建即可。

  • Linux:运行以下命令安装:

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

    此命令会在主机内核中注册模拟器,影响整台主机(重启后失效),请在自己管理的机器上运行。本教程的测试环境没有安装模拟器,因此才会重现上面的错误。

模拟运行的速度可能比原生运行慢好几倍,尤其是需要编译的步骤(例如 npm ci 编译原生模块、pip install 编译 C 扩展)。因此,对于支持交叉编译的语言(Go、Rust),尽量使用交叉编译;其他语言可以考虑在 CI 中使用原生 arm64 运行环境。

推送多平台镜像

推送到 Registry 后,两个平台会一起上传。这里使用本机的 Registry(registry:3,请参阅推送到 Docker Hub)进行演示:

$ docker tag goapp:1.0 localhost:5000/goapp:1.0

$ docker push localhost:5000/goapp:1.0
The push refers to repository [localhost:5000/goapp]
49af9741d5c4: Pushed
2cc7ee286bf3: Pushed
c172f21841df: Pushed
7c12895b777b: Pushed
44136fa355b3: Pushed
f64822ab576c: Pushed
218cf840d0d9: Pushed
f6069939f718: Pushed
d6b1b89eccac: Pushed
2780920e5dbf: Pushed
3214acf345c0: Pushed
52630fc75a18: Pushed
dd64bf2dd177: Pushed
b839dfae01f6: Pushed
ebddc55facdc: Pushed
c4bc6f35ff5e: Pushed
3febe656e709: Pushed
6ed1ef0cb115: Pushed
7f6c568ae1e4: Pushed
401d177fd1e8: Pushed
1.0: digest: sha256:dc8d3f71a9f7adb0de32dd59651adc81dab5b78612cdc7eccc39a5f73608d37e size: 1609

使用 docker buildx imagetools inspect 检查 Registry 上的 manifest:

$ docker buildx imagetools inspect localhost:5000/goapp:1.0
Name:      localhost:5000/goapp:1.0
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:dc8d3f71a9f7adb0de32dd59651adc81dab5b78612cdc7eccc39a5f73608d37e

Manifests:
  Name:        localhost:5000/goapp:1.0@sha256:bae3f4b45be6e6c37bbe5430c1fd68cf98974b82dd312b81fcf568abb321584c
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/amd64

  Name:        localhost:5000/goapp:1.0@sha256:d6cd4f43dd3d332129c7b6b9f6720c6da06e070125860d2ab0661e15d251133a
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    linux/arm64

  Name:        localhost:5000/goapp:1.0@sha256:379134f8a7c50a77e0bf19cbd9593a884daeac7eb2b7c852ccfa8f81ce7bdd34
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    unknown/unknown
  Annotations:
    vnd.docker.reference.digest: sha256:bae3f4b45be6e6c37bbe5430c1fd68cf98974b82dd312b81fcf568abb321584c
    vnd.docker.reference.type:   attestation-manifest

  Name:        localhost:5000/goapp:1.0@sha256:2f4085c9f5da45df6153b09141bf7db573806a93a2ed81c64c2ec75e49bfa8e6
  MediaType:   application/vnd.oci.image.manifest.v1+json
  Platform:    unknown/unknown
  Annotations:
    vnd.docker.reference.digest: sha256:d6cd4f43dd3d332129c7b6b9f6720c6da06e070125860d2ab0661e15d251133a
    vnd.docker.reference.type:   attestation-manifest
  • 最上层是一个 image index(application/vnd.oci.image.index.v1+json),也就是多平台镜像的“目录”。
  • 下面有 linux/amd64 和 linux/arm64 两个镜像。
  • 另外两个 unknown/unknown 是 BuildKit 自动生成的 attestation(构建来源证明),分别对应两个平台,并非可执行镜像。

推送到 Docker Hub 的命令也一样,只需将名称改为 你的账号/goapp:1.0。之后在任何架构的主机上运行 docker pull,都会获取对应的版本。

只构建目标平台

如果只想在 Apple Silicon Mac 上构建供 amd64 服务器使用的镜像,则无需构建多平台镜像,只要指定目标平台即可:

docker build --platform linux/amd64 -t yourname/myapp:1.0 .
docker push yourname/myapp:1.0

Docker Desktop 会使用内置的 QEMU 模拟 amd64,速度比原生运行慢,但可以正常工作。

重点回顾

  • 镜像需要与主机的 CPU 架构匹配,否则会出现 exec format error。
  • 多平台镜像是一个 index,下面包含各个平台的版本。
  • 使用 FROM --platform=$BUILDPLATFORM + TARGETOS/TARGETARCH 进行交叉编译,无需模拟。
  • Docker Desktop 内置 QEMU;Linux 需要安装 binfmt。

动手练习

练习 1:使用 docker buildx imagetools inspect 查询 postgres:18 支持哪些平台。

答案
docker buildx imagetools inspect postgres:18 | grep Platform: | grep -v unknown

unknown/unknown 是构建证明(attestation),不是可执行镜像,因此将其排除。

常见问题

我的 Mac 使用 Apple Silicon,部署到云主机时要注意什么?

构建时添加 --platform linux/amd64,或构建多平台镜像,否则服务器上会出现 exec format error。

多平台构建一定需要 QEMU 吗?

只有在构建过程中需要在目标平台上运行程序(例如 RUN npm ci)时才需要。Go、Rust 这类支持交叉编译的语言不需要。

如何知道一个镜像支持哪些平台?

运行 docker buildx imagetools inspect 镜像。本章以 nginx 为例列出了 8 种平台。

更新于 2026年10月14日