目錄

建置多平台映像檔(amd64 與 arm64):docker buildx 教學

問題: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 網頁服務

用一個會顯示自己執行架構的 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日