Як реалізувати multi-platform builds (ARM + AMD64)?
Multi-platform Docker images це images, побудовані один раз і теґнуті під одним іменем, але містять layer'и, скомпільовані під кілька CPU-архітектур (наприклад, linux/amd64 і linux/arm64). Daemon прозоро pull'ить правильний варіант на runtime. Це важливо, бо Apple Silicon Mac, AWS Graviton-instances і Raspberry Pi всі на ARM, тоді як більшість laptop'ів і CI-runner'ів на x86-64.
Теорія
TL;DR
- Бери
docker buildx build --platform linux/amd64,linux/arm64 -t name --push . - Output це manifest list: один tag, що показує на N per-arch images.
- Buildx за замовчуванням бере QEMU-емуляцію для non-native архітектур. Повільно, але працює.
- Для швидкості setup'и native builder'и per architecture (реальна ARM-машина).
- Не можна зберігати multi-arch локально; треба
--pushу registry. Або використовуй--output=ociдля OCI-bundle. - Типові болі: native-deps, що ламаються під емуляцією,
CGO_ENABLED=0для Go, prebuilt-wheel'и для Python.
Чому multi-platform
Одна CPU-архітектура колись була нормою. Сьогодні:
- Apple Silicon (M1/M2/M3) девелопери крутять ARM локально.
- AWS Graviton, Ampere, Oracle ARM дають 30-40% кращий price/performance.
- Raspberry Pi, IoT, edge потребують ARM-32 або ARM-64.
- Server-farm'и ще переважно x86-64.
Якщо твоя image тільки linux/amd64, Apple Silicon-dev, що pull'ить її, отримає прозору QEMU-емуляцію (повільно), або no matching manifest помилку. Multi-arch фіксить це одним tag-ом.
Manifest list (a.k.a. fat manifest)
Registry зберігає один додатковий manifest per tag, що показує на per-arch-images:
{
"manifests": [
{ "platform": { "architecture": "amd64", "os": "linux" }, "digest": "sha256:abc..." },
{ "platform": { "architecture": "arm64", "os": "linux" }, "digest": "sha256:def..." }
]
}Коли ти docker pull myorg/app:1.0 на ARM-Mac, daemon читає manifest list, бере arm64-digest, pull'ить правильні layer'и. Той самий tag, та сама команда, різні bytes.
Як buildx його будує
Buildkit (engine за buildx) компілює кожну архітектуру в окремому context. Два варіанти:
- Один builder + QEMU. Buildkit реєструє
binfmt_misc, щоб транслювати non-native бінарі через QEMU. Host крутитьRUN-інструкції для кожної платформи через емуляцію. Працює всюди, повільно для скомпільованих мов. - Кілька native-builder'ів. Buildkit розкидає
arm64-роботу на реальну ARM-машину,amd64-роботу на x86-машину. Швидко, потребує інфраструктури.
Приклади
Швидкий старт: емульований multi-arch
# Enable QEMU один раз per host (Docker Desktop робить це автоматично)
docker run --privileged --rm tonistiigi/binfmt --install all
# Створи buildx-builder, що використовує docker-container driver
docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap
# Build для двох платформ, push у registry
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t myorg/app:1.0 \
--push \
.--push обов'язковий, бо multi-arch images не живуть у локальному Docker-image-store (він індексує by single arch). Build пушить обидві архітектури і manifest list за один крок.
Верифікуй:
docker buildx imagetools inspect myorg/app:1.0
# Manifest: docker.io/myorg/app:1.0@sha256:...
# MediaType: application/vnd.oci.image.index.v1+json
# Manifests:
# linux/amd64
# linux/arm64CI-friendly: GitHub Actions
name: build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
platforms: linux/amd64,linux/arm64
push: true
tags: myorg/app:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxsetup-qemu-action реєструє binfmt; cache-from: type=gha переюзає GitHub Actions cache між run'ами (величезний speedup).
Native ARM-builder (production)
Емуляція може бути 10x повільнішою за native. Для реальних workload-ів реєструй окрему ARM-машину:
# На x86 «orchestrator»-host
docker buildx create \
--name multi-native \
--node x86 --platform linux/amd64
docker buildx create \
--append \
--name multi-native \
--node arm \
--platform linux/arm64 \
ssh://user@arm-host
docker buildx use multi-native
docker buildx inspect --bootstrap
# Build: кожна arch крутиться natively на своїй node
docker buildx build --platform linux/amd64,linux/arm64 -t myorg/app:1.0 --push .Тепер linux/amd64-робота крутиться на локальній x86, linux/arm64-робота на remote ARM-box; buildx merge'ить результати в один manifest list.
Build тільки локальної платформи під час dev
# Коли itera'єш, не треба обох архітектур
docker buildx build --platform local -t myorg/app:dev --load .--load затягує результат у локальний image-store (неможливо з multi-arch). Бери --load для dev, --push для release.
Типові пастки
Native-deps ламаються під емуляцією
Python-package, що компілює C-extension, може fail під QEMU, бо build-script детектить host-glibc, але binary емулюється:
fatal error: Python.h: No such file or directory
Фікс: бери prebuilt-wheel'и (PyPI manylinux), або build natively per arch.
Go: CGO_ENABLED і cross-compilation
Go cross-компілює natively без емуляції:
FROM golang:1.22 AS build
ARG TARGETOS TARGETARCH
COPY . /src
WORKDIR /src
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o app
FROM gcr.io/distroless/static
COPY /src/app /app
ENTRYPOINT ["/app"]$BUILDPLATFORM це host-arch (швидко); $TARGETPLATFORM це output-arch. Go компілює без емуляції; лише фінальний layer per-arch.
Різні package-manager'и per arch
FROM alpine:3.18
ARG TARGETARCH
RUN apk add --no-cache libstdc++
# Alpine-repos auto-resolve per arch; екстра-роботи не треба.Але для дистрибутивів без auto-resolution, можливо потрібно:
RUN case "$TARGETARCH" in \
amd64) URL=https://example.com/amd64.tar.gz ;; \
arm64) URL=https://example.com/arm64.tar.gz ;; \
esac && wget -O - "$URL" | tar xzdocker manifest vs buildx
Старіша docker manifest команда може зшити existing per-arch images у manifest list вручну. Buildx автоматизує і це сучасний шлях. Бери buildx, якщо нема legacy-tooling.
Реальне застосування
- Публічні images на Docker Hub: ship multi-arch (Postgres, Redis, nginx всі роблять).
- Внутрішні app: щонайменше amd64 + arm64, якщо хоч один dev на Apple Silicon або хоч один prod на Graviton.
- CI-matrix: бери buildx +
cache-from: type=gha, щоб не rebuild'ити обидві архітектури кожен push. - Edge-деплої: додавай
linux/arm/v7для Raspberry Pi-class пристроїв.
Питання для поглиблення
Q: Чи можу я крутити multi-arch image без buildx?
A: Так, daemon обробляє pull-time селекцію автоматично. Buildx потрібен лише, щоб виробити multi-arch images.
Q: Наскільки повільніша QEMU-емуляція?
A: Для скомпільованих мов (Rust, C++), 5-10x повільніша за native. Для інтерпретованих (Python, Node.js install), 2-3x. Для Go (cross-compile, без емуляції), майже безкоштовно.
Q: Що таке --platform=$BUILDPLATFORM?
A: Каже «крути цю stage на host-arch, незалежно від target». Бери для build-step, що виробляє arch-specific output (Go cross-compile). Фінальна stage використовує $TARGETPLATFORM, щоб assemble per-arch images.
Q: (Senior) Як debug'ити multi-arch build, що ламається тільки для arm64?
A: Build тільки тієї платформи (--platform linux/arm64 --load --platform local після switch builder'а), потім docker run --rm -it --platform linux/arm64 myorg/app:dev sh, щоб порисати у failed-image. Інспектуй логи з docker buildx build --progress=plain для verbose-output. Якщо помилка exec format error, ти скопіював amd64-binary у arm64-stage.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.
Коментарі
Ще немає коментарів