Multi-arch и сборка под ARM: buildx и эмуляция

Рейтинг: 78.5% · 14 голосов
Практический курс по Docker: образы, контейнеры, тома, сети, Compose и продакшен. Уроки по главам с обсуждением.
Ответить
Аватара пользователя
Marina_DevOps
Сообщения: 54
Зарегистрирован: 11 май 2026, 05:31

Multi-arch и сборка под ARM: buildx и эмуляция

Сообщение Marina_DevOps »

Оглавление курса (46)
  1. Что такое Docker и какие задачи он решает
  2. Установка Docker и запуск первого контейнера
  3. Образы: слои, теги и реестр Docker Hub
  4. Пишем свой Dockerfile
  5. Тома и хранение данных: volumes и bind mounts
  6. Сети в Docker: связываем контейнеры между собой
  7. Переменные окружения и конфигурация контейнеров
  8. Docker Compose: поднимаем многоконтейнерное приложение
  9. Оптимизация образов: multi-stage сборка, размер и кэш слоёв
  10. Логи, отладка и мониторинг контейнеров
  11. Базовая безопасность контейнеров
  12. Подготовка к продакшену: что важно учесть
  13. Dockerfile глубже: ENTRYPOINT и CMD, HEALTHCHECK, .dockerignore, запуск не от root
  14. Реестры образов: приватные registry, push и pull, теги и digest, imagePullSecrets
  15. BuildKit и buildx: multi-arch сборки, секреты сборки, экспорт кэша
  16. Docker в CI/CD: автосборка, сканирование образов (Trivy, Docker Scout), публикация
  17. Итоговый проект и куда расти: от Dockerfile до прода, обзор оркестрации (Kubernetes, Podman, OCI)
  18. Архитектура Docker: dockerd, containerd, runc и OCI
  19. Как работает изоляция: namespaces и cgroups
  20. Образы изнутри: слои, OverlayFS и storage driver
  21. Жизненный цикл контейнера и политики перезапуска
  22. Отладка контейнеров: exec, attach, nsenter, debug
  23. Сети Docker глубже: bridge, overlay, macvlan и iptables
  24. Docker Compose глубже: profiles, healthcheck-зависимости, override
  25. Данные и тома глубже: драйверы, бэкап, права
  26. Секреты и конфигурация: как не светить пароли
  27. Ограничение ресурсов: CPU, память, OOM и cgroups
  28. Здоровье и автозапуск: HEALTHCHECK, init и сигналы
  29. Логирование контейнеров: драйверы, ротация, сбор
  30. Мониторинг контейнеров: stats, cAdvisor, Prometheus
  31. Безопасность глубже: rootless, user namespaces, seccomp, capabilities
  32. Supply chain: сканирование, SBOM и подпись образов
  33. Глубокая оптимизация образов: distroless, scratch, кэш
  34. Multi-arch и сборка под ARM: buildx и эмуляция (вы здесь)
  35. Реестры в продакшене: Harbor и облачные registry
  36. Docker без Docker: Podman, containerd, nerdctl, Buildah
  37. От Compose к оркестрации: когда контейнеров становится много
  38. Docker Swarm: встроенная оркестрация
  39. Сборка образов в CI без демона: kaniko, BuildKit, Buildah
  40. Траблшутинг Docker: типичные ошибки и как чинить
  41. Производительность и эксплуатация Docker-хоста
  42. Docker Desktop, WSL2 и альтернативы на Windows и Mac
  43. Контейнеризация приложений: бэкенд, фронтенд, БД
  44. Docker и Kubernetes: containerd, миграция, kompose
  45. Лучшие практики и антипаттерны Docker
  46. Сквозной проект: production-ready стек и путь дальше
Боль: образ собрался, а на сервере "exec format error"

Знакомая ситуация. Ты разрабатываешь на MacBook с Apple Silicon (это ARM, arm64), собрал образ, запушил в registry, выкатил на прод - а там Intel-сервер (amd64). И контейнер падает с загадочным

Код: Выделить всё

exec format error
. Или наоборот: собрал на обычном x86-ноутбуке, а целевой парк - ARM-серверы (Graviton в AWS, Ampere в Oracle Cloud, ARM-инстансы в Yandex Cloud). Бинарь внутри образа скомпилирован под чужую архитектуру, ядро не умеет его запустить, и всё.

Раньше с этим жили костылями: отдельные теги

Код: Выделить всё

myapp:latest-amd64
и

Код: Выделить всё

myapp:latest-arm64
, ручной выбор в каждом docker-compose. Это ад сопровождения. Сегодня правильный ответ - один тег, который сам подставляет нужную архитектуру. Это и есть multi-arch docker: один

Код: Выделить всё

myapp:1.0
, и на Intel-ноде тянется amd64-вариант, а на Graviton - arm64, прозрачно. В этом уроке разберём, как docker buildx собирает такие образы, как работает эмуляция через QEMU, чем нативная сборка на ARM-раннере лучше, и где разложены грабли.

Изображение

Что такое архитектура образа и manifest list

Сначала разберёмся, почему образ вообще привязан к архитектуре. Внутри образа лежат скомпилированные бинари: исполняемый файл твоего приложения, библиотеки libc, утилиты базового образа. Машинный код в них - это инструкции конкретного процессора. Команда для x86 и для ARM выглядит по-разному на уровне байтов. Поэтому образ собранный под amd64 физически не запустится на arm64 - ядро видит чужой ELF-заголовок и отказывается.

Как тогда один тег обслуживает разные процессоры? Через manifest list (по спецификации OCI это называется image index). Это не образ с данными, а маленький JSON-указатель: "под linux/amd64 бери образ с таким-то digest, под linux/arm64 - с таким-то". Когда ты делаешь

Код: Выделить всё

docker pull myapp:1.0
, демон смотрит свою архитектуру, находит в manifest list подходящую запись и тянет только её. Остальное он даже не качает.

Глянем на реальный multi-arch образ:

Код: Выделить всё

docker buildx imagetools inspect alpine:3.20
Сокращённый вывод:

Код: Выделить всё

Name:      docker.io/library/alpine:3.20
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:de4fe7064d8...

Manifests:
  Name:      docker.io/library/alpine:3.20@sha256:1e42bb...
  Platform:  linux/amd64
  Name:      docker.io/library/alpine:3.20@sha256:7a85bf...
  Platform:  linux/arm64/v8
  Name:      docker.io/library/alpine:3.20@sha256:9cee2b...
  Platform:  linux/arm/v7
  Name:      docker.io/library/alpine:3.20@sha256:4f5b2c...
  Platform:  linux/386
Видишь? Один тег

Код: Выделить всё

alpine:3.20
- под капотом четыре разных образа плюс индекс сверху.

Код: Выделить всё

MediaType: ...image.index...
- это и есть manifest list. Поле

Код: Выделить всё

Platform
разбито на ОС, архитектуру и иногда вариант (

Код: Выделить всё

arm64/v8
, - разные поколения и ABI ARM). Именно эту структуру тебе и предстоит собирать.

docker buildx: движок мульти-платформенной сборки

Обычный

Код: Выделить всё

docker build
собирает ровно под архитектуру демона - одну. Чтобы собрать под несколько сразу, нужен docker buildx - расширенный фронтенд над движком BuildKit. В современном Docker buildx идёт из коробки, отдельно ставить не надо. Проверь:

Код: Выделить всё

docker buildx version
# github.com/docker/buildx v0.18.0 ...
Buildx работает не напрямую в демоне, а через builder instance с драйвером. Драйвер по умолчанию (классический встроенный) мульти-платформу и push в registry за один проход не умеет. Поэтому для multi-arch создают builder на драйвере

Код: Выделить всё

docker-container
- BuildKit запускается в отдельном контейнере и получает полный набор фич:

Код: Выделить всё

docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap
делает его текущим,

Код: Выделить всё

--bootstrap
поднимает контейнер BuildKit сразу, чтобы увидеть, какие платформы он умеет. В выводе ключевая строка:

Код: Выделить всё

Platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64,
           linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
Это список того, под что builder готов собирать. Если ты на чистом amd64-хосте, а в списке есть

Код: Выделить всё

linux/arm64
и

Код: Выделить всё

linux/riscv64
- значит уже подключилась эмуляция. Откуда она берётся - в следующей секции.

QEMU и binfmt_misc: как amd64-хост собирает под ARM

Главный фокус: как машина с Intel-процессором выполняет ARM-инструкции во время сборки? Ведь

Код: Выделить всё

RUN apk add ...
внутри arm64-образа реально запускает arm64-бинарь пакетного менеджера. Ответ - программная эмуляция через QEMU в режиме user-mode.

Работает это через механизм ядра Linux binfmt_misc. Это таблица в

Код: Выделить всё

/proc/sys/fs/binfmt_misc/
, где ядро сопоставляет "магические байты" в заголовке исполняемого файла с обработчиком. Регистрируешь правило: "если ELF помечен как arm64 - не пытайся выполнить сам, передай файл в

Код: Выделить всё

qemu-aarch64
". Дальше ядро прозрачно перехватывает запуск чужого бинаря и гонит его через QEMU, который транслирует ARM-инструкции в x86 на лету.

Регистрируется это одной командой - официальный образ ставит обработчики для всех архитектур:

Код: Выделить всё

docker run --privileged --rm tonistiigi/binfmt --install all

Код: Выделить всё

--privileged
тут обязателен: правки в binfmt_misc - это операция на уровне ядра хоста. После установки проверь:

Код: Выделить всё

ls /proc/sys/fs/binfmt_misc/
# qemu-aarch64  qemu-arm  qemu-riscv64  qemu-s390x  status  ...
cat /proc/sys/fs/binfmt_misc/qemu-aarch64
# enabled
# interpreter /usr/bin/qemu-aarch64
# flags: OCF
Теперь на Docker Desktop (Mac, Windows) ничего этого делать обычно не нужно - там QEMU-обработчики уже вшиты в виртуалку. А вот на голом Linux-сервере или в CI-раннере шаг с

Код: Выделить всё

tonistiigi/binfmt
- обязательный, иначе docker arm64 сборка с эмуляцией просто не стартует: BuildKit не найдёт обработчик и упадёт с ошибкой про неизвестную платформу.

Теперь сама сборка под несколько архитектур одной командой:

Код: Выделить всё

docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.ru/myapp:1.0 \
  --push .
Флаг

Код: Выделить всё

--platform
со списком через запятую - это и есть суть multi-arch. BuildKit прогонит Dockerfile дважды (для amd64 нативно, для arm64 через QEMU), соберёт два образа, сам сошьёт manifest list и запушит всё под один тег

Код: Выделить всё

myapp:1.0
. Поле docker platform управляет именно этим набором целей.

Важная деталь про . Multi-platform результат нельзя просто загрузить в локальный демон через обычный : локальное хранилище образов исторически держит один образ под одну архитектуру и не понимает manifest list целиком. Поэтому multi-arch почти всегда идёт сразу в registry. Хочешь оставить локально только одну арку для проверки - собирай её отдельно с и одной платформой.

Эмуляция против нативной сборки: почему QEMU медленный

Эмуляция работает, но за неё платишь скоростью. QEMU транслирует каждую инструкцию чужой архитектуры в инструкции хоста. Для I/O и распаковки пакетов это терпимо, а вот компиляция и тяжёлые CPU-задачи замедляются драматически - в реальных проектах в 5-20 раз. Сборка Go или C-проекта под arm64 на amd64-хосте через QEMU легко превращает двухминутный билд в пятнадцатиминутный. Плюс эмуляция временами нестабильна: те же исходники иногда падают на ровном месте из-за тонких багов трансляции (особенно ловят это сборки с большим объёмом нативного кода, JIT, специфичных syscall).

Есть три способа жить с этим, от худшего к лучшему:
  • Чистая эмуляция. Просто

    Код: Выделить всё

    --platform linux/amd64,linux/arm64
    на одном amd64-раннере. Проще некуда, но медленно и иногда хрупко. Годится для лёгких образов и редких релизов.
  • Нативная сборка на ARM-раннере. Заводишь второй раннер на реальном ARM-железе (Graviton, Ampere, arm64-инстанс облака) и собираешь arm64-часть на нём без QEMU. BuildKit умеет распределять сборку по нескольким нодам одного builder. Реальные кейсы показывают падение времени с 15-20 минут до 2-3. Это золотой стандарт для серьёзного CI.
  • Кросс-компиляция. Компилятор сам генерит чужой машинный код, эмуляция не нужна вообще. Самый быстрый путь для компилируемых языков - о нём ниже.
Практический ориентир: пока эмулированная сборка укладывается в пару минут - живи с QEMU, не усложняй. Как только перевалила за 5 минут и мешает - переходи на нативный ARM-раннер или кросс-компиляцию.

Кросс-компиляция в multi-stage: TARGETPLATFORM и BUILDPLATFORM

Вот где раскрывается настоящая инженерия. BuildKit автоматически прокидывает в Dockerfile несколько служебных ARG, и два главных:
Идея: компилятор запускаем нативно на хосте (быстро, без QEMU), а на выходе просим его собрать бинарь под целевую арку. Для Go это особенно изящно - кросс-компиляция встроена через :

Код: Выделить всё

FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS
ARG TARGETARCH
WORKDIR /src
COPY . .
# компилятор бежит нативно, цель задаём переменными
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
    go build -o /out/app ./cmd/app

FROM alpine:3.20
COPY --from=build /out/app /usr/local/bin/app
ENTRYPOINT ["/app"]
Разбор ключевого трюка. В первой строке

Код: Выделить всё

FROM --platform=$BUILDPLATFORM golang...
мы жёстко прибиваем stage сборки к архитектуре хоста. Без этого

Код: Выделить всё

--platform
BuildKit честно подтянул бы arm64-вариант образа golang и гонял бы весь компилятор через QEMU - медленно. А так компилятор нативный, а

Код: Выделить всё

GOARCH=$TARGETARCH
говорит ему выдать arm64-бинарь. Финальный stage уже идёт под TARGETPLATFORM, и в него мы просто кладём готовый бинарь. Эмуляции - ноль, скорость почти как у нативной сборки. Для Rust то же делается через target triple, для C/C++ - через кросс-тулчейн.

Важно понимать границу: кросс-компиляция спасает только саму компиляцию. Если в целевого stage ты дёргаешь чужеархитектурные бинари (например запускаешь собранное приложение или ставишь пакеты под целевую арку) - там эмуляция всё равно включится. Поэтому держи целевой stage тонким.

Тестирование и публикация multi-arch

Собрали под обе арки - надо проверить, что arm64-образ реально рабочий, а не просто собрался. На amd64-хосте с включённым binfmt можно запустить чужую арку прямо так:

Код: Выделить всё

docker run --rm --platform linux/arm64 registry.example.ru/myapp:1.0 uname -m
# aarch64
Видишь вместо - значит контейнер реально поехал под ARM через QEMU, и базовая работоспособность есть. Но помни: это эмуляция, она медленнее и не ловит всех нюансов реального железа. Полноценные тесты гоняй на настоящем ARM-раннере.

После push проверь, что manifest list собрался правильно:

Код: Выделить всё

docker buildx imagetools inspect registry.example.ru/myapp:1.0
Должны быть обе платформы -

Код: Выделить всё

linux/amd64
и

Код: Выделить всё

linux/arm64
. Если видишь только одну - где-то

Код: Выделить всё

--platform
потерялся или push прошёл частично.

Про RU-реалии. Docker Hub из России временами недоступен или режет лимиты, поэтому base-образы тяни через зеркала, а свои multi-arch образы пушь в Yandex Container Registry или VK Cloud Registry - они держат OCI image index штатно, manifest list публикуется так же. Если registry приватный, не забудь про

Код: Выделить всё

docker login
перед , иначе сборка отработает, а публикация упадёт в самом конце - обидно после долгой эмуляции.

Типичные грабли и антипаттерны
  • Забыл binfmt на чистом Linux/CI. На голом сервере без

    Код: Выделить всё

    tonistiigi/binfmt --install all
    эмуляция не работает, и сборка падает с невнятной ошибкой про платформу. Docker Desktop это прячет - отсюда классика "у меня на маке собиралось, в CI нет".
  • Ждёшь --load для multi-arch. Список платформ + не дружат: локальный демон не примет manifest list. Либо , либо одна платформа.
  • FROM без --platform=$BUILDPLATFORM в build-stage. Тогда компилятор едет через QEMU, и кросс-компиляция теряет весь смысл - билд внезапно медленный. Всегда прибивай stage сборки к BUILDPLATFORM.
  • Тяжёлая компиляция чисто на QEMU. Если язык умеет кросс-компиляцию (Go, Rust, C) - используй её, а не эмулируй весь компилятор. Эмуляцию оставь для интерпретируемого стека (Python, Node), где компилировать нечего.
  • Уверенность, что QEMU == реальное железо. Эмуляция ловит баги ABI и иногда сама их создаёт. Финальный smoke-тест - только на нативном ARM.
  • Один общий тег без проверки манифеста. После релиза всегда делай

    Код: Выделить всё

    imagetools inspect
    : бывает, что под тегом осталась одна арка, и половина прода ловит exec format error.
Мини-лаба: собери multi-arch руками
  • Включи эмуляцию:

    Код: Выделить всё

    docker run --privileged --rm tonistiigi/binfmt --install all
    и проверь

    Код: Выделить всё

    ls /proc/sys/fs/binfmt_misc/
    .
  • Создай builder:

    Код: Выделить всё

    docker buildx create --name multi --driver docker-container --use
    , потом

    Код: Выделить всё

    docker buildx inspect --bootstrap
    - убедись, что в Platforms есть linux/arm64.
  • Возьми Go-Dockerfile из урока с

    Код: Выделить всё

    --platform=$BUILDPLATFORM
    и ARG TARGETOS/TARGETARCH.
  • Собери и запушь:

    Код: Выделить всё

    docker buildx build --platform linux/amd64,linux/arm64 -t <твой-registry>/app:lab --push .
  • Проверь manifest:

    Код: Выделить всё

    docker buildx imagetools inspect <твой-registry>/app:lab
    - должны быть обе арки.
  • Запусти arm64-вариант на своём хосте:

    Код: Выделить всё

    docker run --rm --platform linux/arm64 <твой-registry>/app:lab uname -m
    - жди aarch64.
  • Для контраста замени

    Код: Выделить всё

    --platform=$BUILDPLATFORM
    на пустое и собери arm64 снова - почувствуй, насколько эмуляция компилятора медленнее кросс-компиляции.
Контрольные вопросы
  • Что физически лежит под одним multi-arch тегом и как демон выбирает нужный образ при pull?
  • Зачем нужен

    Код: Выделить всё

    tonistiigi/binfmt --install all
    и какой механизм ядра он настраивает?
  • Чем кросс-компиляция с

    Код: Выделить всё

    --platform=$BUILDPLATFORM
    выгоднее, чем эмуляция компилятора через QEMU?
  • Почему multi-platform сборку обычно делают сразу с , а не ?
Итог

Multi-arch - это не магия, а manifest list поверх нескольких обычных образов плюс умение buildx собрать их за один проход. QEMU с binfmt_misc позволяет амд64-хосту собирать под ARM, но платит за это скоростью и стабильностью. Для серьёзного CI выбирай нативные ARM-раннеры или кросс-компиляцию через TARGETPLATFORM/BUILDPLATFORM, а эмуляцию держи для лёгких случаев. Один тег, который сам подставляет arm64 на Graviton и amd64 на Intel - это и есть зрелый docker buildx workflow, и он избавляет тебя от зоопарка арх-специфичных тегов навсегда.
👍6 ❤️3 🔥2 😄 🤔1
Аватара пользователя
hagens
Сообщения: 1
Зарегистрирован: 04 июн 2026, 10:49

Re: Multi-arch и сборка под ARM: buildx и эмуляция

Сообщение hagens »

Вот это вправило мозги. Месяц мучился с двумя тегами latest-amd64 и latest-arm64, а оказывается buildx сам manifest list клеит. Завтра переделаю CI.
👍 ❤️ 🔥1 😄 🤔
Аватара пользователя
riina
Сообщения: 1
Зарегистрирован: 13 май 2026, 05:41

Re: Multi-arch и сборка под ARM: buildx и эмуляция

Сообщение riina »

А подскажите, для Node-проекта кросс-компиляция же не нужна, там компилировать нечего? Получается чистый QEMU и тут не так больно как с Go?
👍3 ❤️ 🔥 😄 🤔1
Ответить
← Предыдущая глава
Глубокая оптимизация образов: distroless, scratch, кэш
Следующая глава →
Реестры в продакшене: Harbor и облачные registry

Все главы курса «Docker: контейнеризация от основ до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «Docker с нуля»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость