Когда образ нужно запускать на разных архитектурах — обычных x86_64-серверах и ARM-хостах — одного docker build уже недостаточно. В этот момент в игру входят buildx, multi-platform сборка и manifest list в registry.
flowchart LR
subgraph CI["CI / локальная машина"]
B["docker buildx build\n--platform amd64,arm64"]
end
subgraph Registry["Container Registry"]
ML["Manifest List\n:1.4.2"]
IA["Image\nlinux/amd64"]
IB["Image\nlinux/arm64"]
ML --> IA
ML --> IB
end
subgraph Hosts["Runtime"]
HA["x86_64 сервер\ndocker pull → amd64"]
HB["ARM хост\ndocker pull → arm64"]
end
B -->|"--push"| ML
IA -.-> HA
IB -.-> HB
style CI fill:#e8dcc8,stroke:#8b7355
style Registry fill:#f9f3e3,stroke:#8b7355
style Hosts fill:#c9e4c5,stroke:#5b8a5e
style ML fill:#d4c5a9,stroke:#8b7355
style IA fill:#d4c5a9,stroke:#8b7355
style IB fill:#d4c5a9,stroke:#8b7355
style HA fill:#a8d5a2,stroke:#5b8a5e
style HB fill:#a8d5a2,stroke:#5b8a5e
buildx собирает отдельный образ под каждую платформу и публикует manifest list — единый тег, за которым стоят платформенные варианты. При docker pull клиент автоматически выбирает образ, подходящий архитектуре хоста.
Когда нужен multi-arch
Подход полезен, если:
- Один и тот же сервис должен работать на
linux/amd64иlinux/arm64. - Вы публикуете образы для смешанной инфраструктуры: облако, домашние ARM-серверы, edge-узлы, Raspberry Pi, Apple Silicon.
- Хочется поддерживать один тег образа вместо набора отдельных тегов под каждую архитектуру.
Если runtime строго на одной архитектуре, а CI и production совпадают — multi-arch может быть преждевременным усложнением.
Создание builder
Стандартный builder Docker не поддерживает multi-platform. Нужно создать отдельный builder на основе BuildKit:
# Создаём builder с драйвером docker-container (поддерживает multi-platform)
docker buildx create --name multiarch --driver docker-container --use
# Проверяем доступные платформы
docker buildx inspect --bootstrapBuilder создаётся один раз и переиспользуется. Команда --bootstrap сразу запускает контейнер BuildKit и показывает поддерживаемые платформы.
Базовый пример multi-arch сборки
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t registry.company.local/team/app:1.4.2 \
-t registry.company.local/team/app:stable \
--push .В registry уходит не один “универсальный бинарник”, а набор платформенных образов плюс manifest list, который связывает их под общим тегом.
Важный момент: --push здесь обязателен. Multi-platform образы нельзя просто сохранить в локальный Docker — они сразу публикуются в registry. Для локального тестирования нужен другой подход (см. раздел про проверку ниже).
Что важно учесть в Dockerfile
Для Go-приложений часто достаточно стандартных аргументов платформы, которые BuildKit устанавливает автоматически:
# Builder работает на платформе CI-машины, не эмулируется
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder
WORKDIR /app
# TARGETOS и TARGETARCH подставляются BuildKit
# для каждой целевой платформы из --platform
ARG TARGETOS
ARG TARGETARCH
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Кросс-компиляция: Go собирает бинарник под целевую архитектуру,
# но сам компилятор работает нативно на хосте
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH \
go build -ldflags="-s -w" -o server ./cmd/server
FROM alpine:3.23
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /usr/local/bin/server
CMD ["server"]Автоматические переменные BuildKit
BuildKit предоставляет несколько переменных для multi-platform сборки:
| Переменная | Значение | Пример |
|---|---|---|
BUILDPLATFORM |
Платформа, на которой запущена сборка | linux/amd64 |
BUILDOS |
ОС сборки | linux |
BUILDARCH |
Архитектура сборки | amd64 |
TARGETPLATFORM |
Целевая платформа | linux/arm64 |
TARGETOS |
Целевая ОС | linux |
TARGETARCH |
Целевая архитектура | arm64 |
TARGETVARIANT |
Вариант архитектуры | v8, v7 |
Ключевой приём: FROM --platform=$BUILDPLATFORM заставляет builder-stage работать нативно на хост-машине. Кросс-компиляция происходит только на этапе go build, что значительно быстрее, чем эмуляция всего builder через QEMU.
QEMU vs нативные builders
Существует два подхода к сборке под чужую архитектуру:
QEMU-эмуляция
Самый простой вариант. BuildKit использует QEMU для эмуляции целевой архитектуры. Установка:
# Регистрирует бинарные форматы QEMU в ядре
docker run --privileged --rm tonistiigi/binfmt --install allПлюсы: просто настроить, работает на любом CI. Минусы: медленно. Типичное замедление — от 3x до 10x для этапов, которые реально выполняются под эмуляцией (установка пакетов, компиляция C/C++). Для Go с FROM --platform=$BUILDPLATFORM основной выигрыш в том, что builder-stage работает нативно на хосте, а не целиком под эмуляцией.
Нативные remote builders
Для больших проектов или CI с высокой нагрузкой эффективнее использовать нативные builders на каждой архитектуре:
# Создаём builder из двух нативных нод
docker buildx create --name native-multiarch \
--driver docker-container \
--platform linux/amd64 \
--node amd64-builder \
--use
# Добавляем ARM-ноду к тому же builder
docker buildx create --name native-multiarch --append \
--driver docker-container \
--platform linux/arm64 \
--node arm64-builder \
ssh://user@arm-hostКаждая нода собирает образ для своей архитектуры нативно, без эмуляции. Результат: скорость как при обычной сборке.
Когда что выбирать
| Критерий | QEMU | Нативные builders |
|---|---|---|
| Настройка | Одна команда | Нужен ARM-хост + SSH |
| Скорость (pure Go) | Почти без потерь | Без потерь |
| Скорость (CGO, C/C++) | 3-10x медленнее | Нативная |
| Скорость (apt/apk install) | 2-5x медленнее | Нативная |
| CI | Работает на любом runner | Нужен self-hosted ARM runner |
Для pure-Go сервисов с FROM --platform=$BUILDPLATFORM разница обычно незначительна. Если сборка под ARM через QEMU занимает больше 5-7 минут — стоит подумать о нативном builder.
Кэширование сборки в CI
Без кэша каждая сборка в CI начинается с нуля. buildx поддерживает несколько бэкендов для кэширования:
Registry cache
Кэш хранится как отдельный образ в том же registry:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=registry.company.local/team/app:buildcache \
--cache-to type=registry,ref=registry.company.local/team/app:buildcache,mode=max \
-t registry.company.local/team/app:1.4.2 \
--push .mode=max кэширует все промежуточные слои, а не только финальный образ. Это важно для multi-stage, где builder-слой переиспользуется чаще всего.
GitHub Actions cache
Для GitHub Actions есть встроенная интеграция:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=gha \
--cache-to type=gha,mode=max \
-t registry.company.local/team/app:1.4.2 \
--push .Локальный кэш
Для self-hosted runners, где файловая система сохраняется между запусками:
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=local,src=/tmp/buildx-cache \
--cache-to type=local,dest=/tmp/buildx-cache,mode=max \
-t registry.company.local/team/app:1.4.2 \
--push .Пример для GitHub Actions
Полный pipeline с QEMU, кэшированием и push:
name: docker-multi-arch
on:
push:
branches: [main]
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# QEMU нужен для эмуляции ARM на x86_64 runner
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
# Создаёт builder с поддержкой multi-platform
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: registry.company.local
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
registry.company.local/team/app:${{ github.sha }}
registry.company.local/team/app:latest
# GitHub Actions cache для ускорения повторных сборок
cache-from: type=gha
cache-to: type=gha,mode=maxПроверка multi-arch образов
После push важно убедиться, что manifest list содержит все нужные платформы.
Проверка manifest list
# Показывает все платформы в manifest list
docker buildx imagetools inspect registry.company.local/team/app:1.4.2
# Более компактный вывод через docker manifest
docker manifest inspect registry.company.local/team/app:1.4.2Пример вывода imagetools inspect:
Name: registry.company.local/team/app:1.4.2
MediaType: application/vnd.oci.image.index.v1+json
Manifests:
Name: registry.company.local/team/app:1.4.2@sha256:abc123...
Platform: linux/amd64
Name: registry.company.local/team/app:1.4.2@sha256:def456...
Platform: linux/arm64Локальное тестирование конкретной платформы
Multi-platform образы нельзя загрузить в локальный Docker целиком, но можно собрать и загрузить одну платформу:
# Собрать и загрузить только arm64-вариант для тестирования
docker buildx build \
--platform linux/arm64 \
--load \
-t app:test-arm64 .
# Запустить через QEMU (если хост — amd64)
docker run --rm app:test-arm64 uname -m
# aarch64Что бывает сложным
CGO и нативные зависимости
Pure Go с CGO_ENABLED=0 кросс-компилируется тривиально. Проблемы начинаются, когда нужен CGO:
FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
ARG TARGETARCH
# Установка кросс-компилятора для целевой архитектуры
RUN apt-get update && apt-get install -y \
gcc-aarch64-linux-gnu \
gcc-x86-64-linux-gnu
# Выбор компилятора в зависимости от TARGETARCH (arm64 / amd64)
ENV CC_arm64=aarch64-linux-gnu-gcc
ENV CC_amd64=x86_64-linux-gnu-gcc
WORKDIR /app
COPY . .
# eval подставляет имя переменной CC_arm64 или CC_amd64
# в зависимости от целевой архитектуры
RUN CGO_ENABLED=1 GOARCH=$TARGETARCH \
CC=$(eval echo \$CC_${TARGETARCH}) \
go build -o server ./cmd/serverЭто заметно сложнее. Если можно обойтись без CGO — обходитесь.
Разные базовые образы по архитектурам
Иногда нужны разные пакеты или конфигурация для разных архитектур:
FROM alpine:3.23
ARG TARGETARCH
# Установка архитектурно-зависимых пакетов
RUN if [ "$TARGETARCH" = "arm64" ]; then \
apk --no-cache add libseccomp; \
fiТестирование на CI
Если CI-runner только на amd64, arm64-вариант можно протестировать через QEMU:
- name: Test amd64
run: |
docker buildx build --platform linux/amd64 --load -t app:test-amd64 .
docker run --rm app:test-amd64 /usr/local/bin/server --version
- name: Test arm64 (via QEMU)
run: |
docker buildx build --platform linux/arm64 --load -t app:test-arm64 .
docker run --rm app:test-arm64 /usr/local/bin/server --versionТипичные ошибки
-
FROMбез--platform=$BUILDPLATFORMв builder-stage. Без этого BuildKit эмулирует весь builder через QEMU, что в разы медленнее. Для Go кросс-компиляция черезGOOS/GOARCHвсегда быстрее, чем эмуляция компилятора. -
Забытый
--push. Multi-platform образы нельзя сохранить локально через--load— только в registry. Попытка--loadс несколькими платформами завершится ошибкой. -
Сборка без кэша в CI. Каждая multi-arch сборка удваивает работу (два образа вместо одного). Без
--cache-from/--cache-toвремя CI растёт линейно с количеством платформ. -
Непроверенный manifest list. После push стоит проверить
docker buildx imagetools inspect, чтобы убедиться, что обе платформы на месте. Молча сломанный manifest — неприятный сюрприз при деплое на ARM. -
QEMU для тяжёлых сборок без альтернатив. Если сборка ARM-варианта занимает 15+ минут через QEMU, а amd64 собирается за 3 — это сигнал перейти на нативный remote builder или разделить pipeline.
-
Раздельные single-platform сборки вместо одной multi-arch. Если amd64 и arm64 собираются разными pipeline и каждый пушит в один тег — второй push перезаписывает manifest первого. Результат: тег содержит только одну платформу. Multi-arch сборка должна быть атомарной: один
buildx build --platformс обеими платформами.
Вывод
Multi-arch через buildx — не сложная магия, а стандартный инструмент для смешанной инфраструктуры. Для pure-Go сервисов достаточно FROM --platform=$BUILDPLATFORM, правильных TARGETOS/TARGETARCH и --push в CI. Для более сложных сценариев с CGO или нативными зависимостями нужны дополнительные усилия, но подход остаётся тем же.
Главное правило: если runtime строго на одной архитектуре — не усложняйте. Multi-arch окупается, когда образ действительно должен работать на разных платформах.
Комментарии