Skip to main content

Як реалізувати 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:

json
{ "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. Два варіанти:

  1. Один builder + QEMU. Buildkit реєструє binfmt_misc, щоб транслювати non-native бінарі через QEMU. Host крутить RUN-інструкції для кожної платформи через емуляцію. Працює всюди, повільно для скомпільованих мов.
  2. Кілька native-builder'ів. Buildkit розкидає arm64-роботу на реальну ARM-машину, amd64-роботу на x86-машину. Швидко, потребує інфраструктури.

Приклади

Швидкий старт: емульований multi-arch

bash
# 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 за один крок.

Верифікуй:

bash
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/arm64

CI-friendly: GitHub Actions

yaml
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=max

setup-qemu-action реєструє binfmt; cache-from: type=gha переюзає GitHub Actions cache між run'ами (величезний speedup).

Native ARM-builder (production)

Емуляція може бути 10x повільнішою за native. Для реальних workload-ів реєструй окрему ARM-машину:

bash
# На 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

bash
# Коли 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 без емуляції:

dockerfile
FROM --platform=$BUILDPLATFORM 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 --from=build /src/app /app ENTRYPOINT ["/app"]

$BUILDPLATFORM це host-arch (швидко); $TARGETPLATFORM це output-arch. Go компілює без емуляції; лише фінальний layer per-arch.

Різні package-manager'и per arch

dockerfile
FROM alpine:3.18 ARG TARGETARCH RUN apk add --no-cache libstdc++ # Alpine-repos auto-resolve per arch; екстра-роботи не треба.

Але для дистрибутивів без auto-resolution, можливо потрібно:

dockerfile
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 xz

docker 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.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Коментарі

Ще немає коментарів