Знакомая ситуация. Ты разрабатываешь на MacBook с Apple Silicon (это ARM, arm64), собрал образ, запушил в registry, выкатил на прод - а там Intel-сервер (amd64). И контейнер падает с загадочным
Код: Выделить всё
exec format errorРаньше с этим жили костылями: отдельные теги
Код: Выделить всё
myapp:latest-amd64Код: Выделить всё
myapp:latest-arm64Код: Выделить всё
myapp:1.0
Что такое архитектура образа и manifest list
Сначала разберёмся, почему образ вообще привязан к архитектуре. Внутри образа лежат скомпилированные бинари: исполняемый файл твоего приложения, библиотеки libc, утилиты базового образа. Машинный код в них - это инструкции конкретного процессора. Команда
Код: Выделить всё
ADDКак тогда один тег обслуживает разные процессоры? Через manifest list (по спецификации OCI это называется image index). Это не образ с данными, а маленький JSON-указатель: "под linux/amd64 бери образ с таким-то digest, под linux/arm64 - с таким-то". Когда ты делаешь
Код: Выделить всё
docker pull myapp:1.0Глянем на реальный 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...Код: Выделить всё
PlatformКод: Выделить всё
arm64/v8Код: Выделить всё
arm/v7docker buildx: движок мульти-платформенной сборки
Обычный
Код: Выделить всё
docker buildКод: Выделить всё
docker buildx version
# github.com/docker/buildx v0.18.0 ...
Код: Выделить всё
dockerКод: Выделить всё
docker-containerКод: Выделить всё
docker buildx create --name multi --driver docker-container --use
docker buildx inspect --bootstrap
Код: Выделить всё
--useКод: Выделить всё
--bootstrapКод: Выделить всё
inspectКод: Выделить всё
Platforms: linux/amd64, linux/amd64/v2, linux/arm64, linux/riscv64,
linux/ppc64le, linux/s390x, linux/386, linux/arm/v7, linux/arm/v6
Код: Выделить всё
linux/arm64Код: Выделить всё
linux/riscv64QEMU и binfmt_misc: как amd64-хост собирает под ARM
Главный фокус: как машина с Intel-процессором выполняет ARM-инструкции во время сборки? Ведь
Код: Выделить всё
RUN apk add ...Работает это через механизм ядра Linux binfmt_misc. Это таблица в
Код: Выделить всё
/proc/sys/fs/binfmt_misc/Код: Выделить всё
qemu-aarch64Регистрируется это одной командой - официальный образ ставит обработчики для всех архитектур:
Код: Выделить всё
docker run --privileged --rm tonistiigi/binfmt --install all
Код: Выделить всё
--privilegedКод: Выделить всё
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
Код: Выделить всё
tonistiigi/binfmtТеперь сама сборка под несколько архитектур одной командой:
Код: Выделить всё
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.example.ru/myapp:1.0 \
--push .
Код: Выделить всё
--platformКод: Выделить всё
myapp:1.0Важная деталь про
Код: Выделить всё
--pushКод: Выделить всё
--loadКод: Выделить всё
--pushКод: Выделить всё
--loadЭмуляция против нативной сборки: почему QEMU медленный
Эмуляция работает, но за неё платишь скоростью. QEMU транслирует каждую инструкцию чужой архитектуры в инструкции хоста. Для I/O и распаковки пакетов это терпимо, а вот компиляция и тяжёлые CPU-задачи замедляются драматически - в реальных проектах в 5-20 раз. Сборка Go или C-проекта под arm64 на amd64-хосте через QEMU легко превращает двухминутный билд в пятнадцатиминутный. Плюс эмуляция временами нестабильна: те же исходники иногда падают на ровном месте из-за тонких багов трансляции (особенно ловят это сборки с большим объёмом нативного кода, JIT, специфичных syscall).
Есть три способа жить с этим, от худшего к лучшему:
- Чистая эмуляция. Просто на одном amd64-раннере. Проще некуда, но медленно и иногда хрупко. Годится для лёгких образов и редких релизов.
Код: Выделить всё
--platform linux/amd64,linux/arm64 - Нативная сборка на ARM-раннере. Заводишь второй раннер на реальном ARM-железе (Graviton, Ampere, arm64-инстанс облака) и собираешь arm64-часть на нём без QEMU. BuildKit умеет распределять сборку по нескольким нодам одного builder. Реальные кейсы показывают падение времени с 15-20 минут до 2-3. Это золотой стандарт для серьёзного CI.
- Кросс-компиляция. Компилятор сам генерит чужой машинный код, эмуляция не нужна вообще. Самый быстрый путь для компилируемых языков - о нём ниже.
Кросс-компиляция в multi-stage: TARGETPLATFORM и BUILDPLATFORM
Вот где раскрывается настоящая инженерия. BuildKit автоматически прокидывает в Dockerfile несколько служебных ARG, и два главных:
- - платформа машины, которая собирает (твой раннер, обычно amd64).
Код: Выделить всё
BUILDPLATFORM - - платформа, под которую собираем прямо сейчас (например linux/arm64). Плюс производные
Код: Выделить всё
TARGETPLATFORM,Код: Выделить всё
TARGETOS,Код: Выделить всё
TARGETARCH.Код: Выделить всё
TARGETVARIANT
Код: Выделить всё
GOARCHКод: Выделить всё
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...Код: Выделить всё
--platformКод: Выделить всё
GOARCH=$TARGETARCHВажно понимать границу: кросс-компиляция спасает только саму компиляцию. Если в
Код: Выделить всё
RUNТестирование и публикация multi-arch
Собрали под обе арки - надо проверить, что arm64-образ реально рабочий, а не просто собрался. На amd64-хосте с включённым binfmt можно запустить чужую арку прямо так:
Код: Выделить всё
docker run --rm --platform linux/arm64 registry.example.ru/myapp:1.0 uname -m
# aarch64
Код: Выделить всё
aarch64Код: Выделить всё
x86_64После push проверь, что manifest list собрался правильно:
Код: Выделить всё
docker buildx imagetools inspect registry.example.ru/myapp:1.0
Код: Выделить всё
linux/amd64Код: Выделить всё
linux/arm64Код: Выделить всё
--platformПро RU-реалии. Docker Hub из России временами недоступен или режет лимиты, поэтому base-образы тяни через зеркала, а свои multi-arch образы пушь в Yandex Container Registry или VK Cloud Registry - они держат OCI image index штатно, manifest list публикуется так же. Если registry приватный, не забудь про
Код: Выделить всё
docker loginКод: Выделить всё
--pushТипичные грабли и антипаттерны
- Забыл binfmt на чистом Linux/CI. На голом сервере без эмуляция не работает, и сборка падает с невнятной ошибкой про платформу. Docker Desktop это прячет - отсюда классика "у меня на маке собиралось, в CI нет".
Код: Выделить всё
tonistiigi/binfmt --install all - Ждёшь --load для multi-arch. Список платформ + не дружат: локальный демон не примет manifest list. Либо
Код: Выделить всё
--load, либо одна платформа.Код: Выделить всё
--push - FROM без --platform=$BUILDPLATFORM в build-stage. Тогда компилятор едет через QEMU, и кросс-компиляция теряет весь смысл - билд внезапно медленный. Всегда прибивай stage сборки к BUILDPLATFORM.
- Тяжёлая компиляция чисто на QEMU. Если язык умеет кросс-компиляцию (Go, Rust, C) - используй её, а не эмулируй весь компилятор. Эмуляцию оставь для интерпретируемого стека (Python, Node), где компилировать нечего.
- Уверенность, что QEMU == реальное железо. Эмуляция ловит баги ABI и иногда сама их создаёт. Финальный smoke-тест - только на нативном ARM.
- Один общий тег без проверки манифеста. После релиза всегда делай : бывает, что под тегом осталась одна арка, и половина прода ловит exec format error.
Код: Выделить всё
imagetools inspect
- Включи эмуляцию: и проверь
Код: Выделить всё
docker run --privileged --rm tonistiigi/binfmt --install all.Код: Выделить всё
ls /proc/sys/fs/binfmt_misc/ - Создай builder: , потом
Код: Выделить всё
docker buildx create --name multi --driver docker-container --use- убедись, что в Platforms есть linux/arm64.Код: Выделить всё
docker buildx inspect --bootstrap - Возьми Go-Dockerfile из урока с и ARG TARGETOS/TARGETARCH.
Код: Выделить всё
--platform=$BUILDPLATFORM - Собери и запушь:
Код: Выделить всё
docker buildx build --platform linux/amd64,linux/arm64 -t <твой-registry>/app:lab --push . - Проверь manifest: - должны быть обе арки.
Код: Выделить всё
docker buildx imagetools inspect <твой-registry>/app:lab - Запусти arm64-вариант на своём хосте: - жди aarch64.
Код: Выделить всё
docker run --rm --platform linux/arm64 <твой-registry>/app:lab uname -m - Для контраста замени на пустое и собери arm64 снова - почувствуй, насколько эмуляция компилятора медленнее кросс-компиляции.
Код: Выделить всё
--platform=$BUILDPLATFORM
- Что физически лежит под одним multi-arch тегом и как демон выбирает нужный образ при pull?
- Зачем нужен и какой механизм ядра он настраивает?
Код: Выделить всё
tonistiigi/binfmt --install all - Чем кросс-компиляция с выгоднее, чем эмуляция компилятора через QEMU?
Код: Выделить всё
--platform=$BUILDPLATFORM - Почему multi-platform сборку обычно делают сразу с , а не
Код: Выделить всё
--push?Код: Выделить всё
--load
Multi-arch - это не магия, а manifest list поверх нескольких обычных образов плюс умение buildx собрать их за один проход. QEMU с binfmt_misc позволяет амд64-хосту собирать под ARM, но платит за это скоростью и стабильностью. Для серьёзного CI выбирай нативные ARM-раннеры или кросс-компиляцию через TARGETPLATFORM/BUILDPLATFORM, а эмуляцию держи для лёгких случаев. Один тег, который сам подставляет arm64 на Graviton и amd64 на Intel - это и есть зрелый docker buildx workflow, и он избавляет тебя от зоопарка арх-специфичных тегов навсегда.