Docker 上級 #2 マルチアーキテクチャイメージ — amd64 と arm64 を一束に

読了 7分

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 #

こんな経験を一度はします。

ローカル (M1/M2/M3)
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 error

exec format error。運用サーバが受け取ったバイナリが arm64 用 で amd64 CPU が実行できなかったのです。Apple Silicon の Docker はデフォルトで arm64 イメージを作ります。push したらそのまま上がり、運用で取得したら互換変形がないので arm64 が amd64 ホストで実行を試みた結果です。

解決は二つの分かれ道:

  1. 単一 amd64 イメージを強制で作る--platform linux/amd64
  2. 二つのアーキテクチャ全てを束ねたマルチアーキイメージを作る--platform linux/amd64,linux/arm64

運用環境が一種類なら 1、二種類以上または将来どこに行くか分からなければ 2 が定石です。最近はほぼ常に 2。

Manifest list — 一つのタグが複数のイメージを指す構造 #

マルチアーキイメージの正体から見ましょう。ghcr.io/me/myapp:1.0 という一つのタグが、実は複数のイメージを束ねた manifest list (OCI 仕様では image index) であり得ます。

manifest list の構造
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 で覗くと明確になります。

manifest list インスペクト
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 .

ここで押さえる二つの事実:

  1. --load は単一プラットフォームだけ 可。マルチプラットフォームは --push または --output type=oci,...
  2. 単一ホストで二つのアーキを作るのは片方が 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 コンテナを動かせる ようにしてくれます。

binfmt 登録 (一度だけ)
docker run --privileged --rm tonistiigi/binfmt --install all

Docker 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 + arm64
# 最初のノード (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)、二種類のランナーでビルドした結果を最後に合わせるパターンも一般化しました。

.github/workflows/multi-arch.yml (要約)
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.0

2 番が上で見た 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.0

somebase:1.0 が amd64 だけならば arm64 ビルドの時点で manifest が見つからず失敗します。ビルド前にベースのプラットフォームサポートを確認:

ベース点検
docker buildx imagetools inspect somebase:1.0

広く使われる公式イメージ (pythonnodegolangnginxpostgres) は普通マルチアーキを全てサポートします。

3. 片方のアーキでだけ通るテスト #

ビルド中に RUN go test ... のような段階があれば、両方のアーキで 動くか確認する必要があります。あるライブラリは amd64-only アセンブリを使ったりするので、片方だけで壊れる可能性があります。

4. 結果検証なし #

CI が --push 後に結果を検証しないとバグを掴めません。一行加えるのが安全:

CI の最後の検証
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 #

ランタイムに特定アーキを強制で受け取ることもできます。

amd64 強制
docker run --platform linux/amd64 myapp

Apple 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 のような道具でイメージの既知の脆弱性をスキャンする話です。

X