Docker 上級 #2 マルチアーキテクチャイメージ — amd64 と arm64 を一束に
Apple Silicon マシンが一般化し、AWS Graviton (ARM) のような ARM 運用環境も定着した今 — 一つのイメージが 二つのアーキテクチャ両方で動く ことが標準になりました。この記事はそのテーマを本格的に扱います。
Docker 上級 シリーズでこの記事の位置:
- #1 BuildKit と buildx
- #2 マルチアーキテクチャイメージ — linux/amd64 + linux/arm64 ← この記事
- #3 イメージセキュリティ — non-root, distroless, scan(Trivy)
- #4 SBOM と署名 (cosign)
- #5 リソース制限と cgroups
- #6 プロダクション運用 — restart 方針、healthcheck、graceful shutdown
最も典型的な事故 — Apple Silicon ↔ amd64 #
こんな経験を一度はします。
docker build -t myapp:1.0 .
docker push ghcr.io/me/myapp:1.0
# 運用サーバ (amd64)
docker pull ghcr.io/me/myapp:1.0
docker run myapp:1.0
# exec /myapp: exec format errorexec format error。運用サーバが受け取ったバイナリが arm64 用 で amd64 CPU が実行できなかったのです。Apple Silicon の Docker はデフォルトで arm64 イメージを作ります。push したらそのまま上がり、運用で取得したら互換変形がないので arm64 が amd64 ホストで実行を試みた結果です。
解決は二つの分かれ道:
- 単一 amd64 イメージを強制で作る —
--platform linux/amd64 - 二つのアーキテクチャ全てを束ねたマルチアーキイメージを作る —
--platform linux/amd64,linux/arm64
運用環境が一種類なら 1、二種類以上または将来どこに行くか分からなければ 2 が定石です。最近はほぼ常に 2。
Manifest list — 一つのタグが複数のイメージを指す構造 #
マルチアーキイメージの正体から見ましょう。ghcr.io/me/myapp:1.0 という一つのタグが、実は複数のイメージを束ねた manifest list (OCI 仕様では image index) であり得ます。
ghcr.io/me/myapp:1.0 ← manifest list (image index)
│
├── linux/amd64 → image manifest @sha256:aaa...
├── linux/arm64 → image manifest @sha256:bbb...
└── linux/arm/v7 → image manifest @sha256:ccc...Docker クライアントが pull するとき、自分のホストの OS / アーキに合うマニフェストを自動で選んでダウンロードします。ユーザーは単一タグ一行だけ知ればよいのです。
docker buildx imagetools で覗くと明確になります。
docker buildx imagetools inspect python:3.14
# Name: docker.io/library/python:3.14
# MediaType: application/vnd.oci.image.index.v1+json
# Digest: sha256:abc...
#
# Manifests:
# Name: docker.io/library/python:3.14@sha256:111...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/amd64
#
# Name: docker.io/library/python:3.14@sha256:222...
# MediaType: application/vnd.oci.image.manifest.v1+json
# Platform: linux/arm64
#
# ... (linux/arm/v7, linux/386, linux/ppc64le, linux/s390x ...)公式イメージは普通 6~8 個のアーキテクチャを同時にサポートします。
ビルド — 二つのアーキを同時に #
buildx + docker-container ドライバの上で一つのコマンドで作ります。
# 最初: docker-container ドライバビルダーを作る ([#1] 参考)
docker buildx create --name multi --driver docker-container --use --bootstrap
# ビルド + push
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/me/myapp:1.0 \
--push .ここで押さえる二つの事実:
--loadは単一プラットフォームだけ 可。マルチプラットフォームは--pushまたは--output type=oci,...で- 単一ホストで二つのアーキを作るのは片方が emulation で動く — だから遅い可能性がある
どのプラットフォームが可能か #
docker buildx inspect multi
# ...
# Platforms: linux/amd64, linux/arm64, linux/arm/v7,
# linux/arm/v6, linux/386, linux/ppc64le, linux/s390xデフォルトの docker-container ドライバは BuildKit コンテナの中に QEMU emulation が一緒に入っているので上の全プラットフォームをビルドできます。ホスト CPU と違うアーキは emulation で動くわけです。
QEMU emulation — 可能か、速いか #
QEMU は一つのアーキの上で別のアーキのコードを動かすエミュレータです。Docker は Linux の binfmt_misc と QEMU を束ねて、amd64 ホストで arm64 コンテナを動かせる ようにしてくれます。
docker run --privileged --rm tonistiigi/binfmt --install allDocker Desktop はこの登録を自動でやってくれます。Linux CI 環境では明示的にする必要があるときがあります。
コスト #
| 作業 | 同じアーキ | QEMU emulation |
|---|---|---|
apt-get install | 速い | 2~5 倍遅い |
| コンパイル (gcc, tsc) | 速い | 3~10 倍遅い |
小さなスクリプト / COPY | 速い | ほぼ同じ |
体感差はビルドの重さによって大きくなります。軽い Python/Node アプリは emulation だけで十分、重いコンパイルが入る Rust/Go/C ビルドは emulation がとても遅い。後者は次の節のネイティブビルダーが答えです。
ネイティブマルチアーキビルダー — --append
#
真剣にマルチアーキビルドを回すなら、各アーキごとにネイティブマシンを置くのが最速です。buildx は一つの builder に複数のノード (マシン) を登録する機能があります。
# 最初のノード (amd64 マシンで)
docker buildx create --name native --node native-amd64 --platform linux/amd64
# 二つ目のノード追加 (arm64 マシンを SSH で登録)
docker buildx create --append \
--name native \
--node native-arm64 \
--platform linux/arm64 \
ssh://ubuntu@arm64-host
docker buildx use native
docker buildx inspect --bootstrapこのビルダーで --platform linux/amd64,linux/arm64 ビルドを回すと、各プラットフォームを そのアーキマシンがネイティブにビルド します。結果は自動で manifest list として束ねられます。
GitHub Actions では ARM ランナーが無料で提供され始めて (ubuntu-24.04-arm)、二種類のランナーでビルドした結果を最後に合わせるパターンも一般化しました。
jobs:
build-amd64:
runs-on: ubuntu-24.04
# ... amd64 ビルド、digest 出力
build-arm64:
runs-on: ubuntu-24.04-arm
# ... arm64 ビルド、digest 出力
manifest:
needs: [build-amd64, build-arm64]
# docker buildx imagetools create で二つの digest を一つの manifest list に各マトリックスジョブが自分のアーキだけネイティブでビルドし、最後に束ねるだけ — 最も速いマルチアーキ CI パターンです。
docker buildx imagetools — 合わせて検証する道具
#
imagetools コマンドはイメージ / manifest list を すでにビルドされた状態で 扱う道具です。
# 1) manifest list 検証 (上で見たコマンド)
docker buildx imagetools inspect ghcr.io/me/myapp:1.0
# 2) 二つのイメージを一つの manifest list に束ねる
docker buildx imagetools create \
-t ghcr.io/me/myapp:1.0 \
ghcr.io/me/myapp:1.0-amd64 \
ghcr.io/me/myapp:1.0-arm64
# 3) タグ移動 (軽い変更)
docker buildx imagetools create \
-t ghcr.io/me/myapp:latest \
ghcr.io/me/myapp:1.02 番が上で見た GHA パターンの最後の段階です。各ジョブが別々のイメージとして push した後、最後のジョブが imagetools create で一つの manifest list に束ねる流れ。
よく出会う落とし穴 #
1. --platform を渡さなくてホストアーキだけ push される
#
docker buildx build --push -t myapp . # ホストアーキだけ push されるdocker-container ドライバでも --platform を渡さないとホストアーキ一つだけビルド/push します。マルチアーキを狙うなら常に --platform 明示。
2. ベースイメージが片方しかサポートしない #
FROM somebase:1.0somebase:1.0 が amd64 だけならば arm64 ビルドの時点で manifest が見つからず失敗します。ビルド前にベースのプラットフォームサポートを確認:
docker buildx imagetools inspect somebase:1.0広く使われる公式イメージ (python、node、golang、nginx、postgres) は普通マルチアーキを全てサポートします。
3. 片方のアーキでだけ通るテスト #
ビルド中に RUN go test ... のような段階があれば、両方のアーキで 動くか確認する必要があります。あるライブラリは amd64-only アセンブリを使ったりするので、片方だけで壊れる可能性があります。
4. 結果検証なし #
CI が --push 後に結果を検証しないとバグを掴めません。一行加えるのが安全:
docker buildx imagetools inspect ghcr.io/me/myapp:${TAG} \
--format '{{range .Manifest.Manifests}}{{.Platform.OS}}/{{.Platform.Architecture}}{{"\n"}}{{end}}'
# linux/amd64
# linux/arm64期待したプラットフォームが全部入っているかを目で確認するだけの手順です。
一コンテナのアーキを強制 — --platform on run
#
ランタイムに特定アーキを強制で受け取ることもできます。
docker run --platform linux/amd64 myappApple Silicon で amd64 イメージが必要な状況 (例: amd64-only な特定の DB クライアント) でよく使います。この場合ホストが emulation で動かすのでパフォーマンスが落ちます。
コンパイルされた言語 — 静的 vs 動的 #
マルチアーキでときどき出会う問題です。glibc 動的リンクバイナリは OS の libc と互換である必要があります。静的バイナリ はこの依存がないので一度ビルドすればどこでも動きますが、alpine の musl libc と glibc がこういう場面で衝突します。
+ Go (CGO_ENABLED=0) 静的 — glibc/musl 無関係、どこでも
+ Rust (musl target) 静的 — alpine ベースで自然
+ Python wheel glibc/musl ごとに別々に作られる
+ Node native modules 同じアーキ + 同じ libc であるべき
- glibc コンパイルされた C 拡張 → musl(alpine) で動かないマルチアーキビルド + slim ベース (= glibc) が一般のアプリのデフォルト組み合わせです。alpine は魅力的ですが、ここまで見た要素と掛け合わさると落とし穴が増えます。
まとめ #
この記事で掴んだ絵:
- マルチアーキイメージは一つのタグが manifest list (OCI image index) で複数の単一マニフェストを束ねたもの
docker-containerドライバビルダー +--platform linux/amd64,linux/arm64が標準パターン- マルチプラットフォームは
--load不可 —--pushまたは OCI 出力 - QEMU emulation は可能だが重いコンパイルで遅くなります。本気なら ネイティブマルチアーキビルダー (または GHA の amd64+arm64 マトリックス + imagetools 結合)
docker buildx imagetoolsで結果を検証 / 結合 / タグ移動- ベースイメージのプラットフォームサポート / 片方のアーキでだけ動くコード / CI 検証漏れがよくある落とし穴
次の記事 (#3 イメージセキュリティ — non-root, distroless, scan(Trivy)) ではこのビルドインフラの上に セキュリティ感覚 を重ねます。非特権ユーザ、読み取り専用ルート、capabilities drop、そして Trivy のような道具でイメージの既知の脆弱性をスキャンする話です。