Docker Buildx и multi-arch образы: одна сборка для amd64 и arm64

Как собирать и публиковать multi-arch Docker-образы через buildx для amd64 и arm64: manifest list, кэширование, QEMU vs нативные builders и проверка результата

Когда образ нужно запускать на разных архитектурах — обычных 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

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

Подход полезен, если:

  1. Один и тот же сервис должен работать на linux/amd64 и linux/arm64.
  2. Вы публикуете образы для смешанной инфраструктуры: облако, домашние ARM-серверы, edge-узлы, Raspberry Pi, Apple Silicon.
  3. Хочется поддерживать один тег образа вместо набора отдельных тегов под каждую архитектуру.

Если 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 --bootstrap

Builder создаётся один раз и переиспользуется. Команда --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

Типичные ошибки

  1. FROM без --platform=$BUILDPLATFORM в builder-stage. Без этого BuildKit эмулирует весь builder через QEMU, что в разы медленнее. Для Go кросс-компиляция через GOOS/GOARCH всегда быстрее, чем эмуляция компилятора.

  2. Забытый --push. Multi-platform образы нельзя сохранить локально через --load — только в registry. Попытка --load с несколькими платформами завершится ошибкой.

  3. Сборка без кэша в CI. Каждая multi-arch сборка удваивает работу (два образа вместо одного). Без --cache-from/--cache-to время CI растёт линейно с количеством платформ.

  4. Непроверенный manifest list. После push стоит проверить docker buildx imagetools inspect, чтобы убедиться, что обе платформы на месте. Молча сломанный manifest — неприятный сюрприз при деплое на ARM.

  5. QEMU для тяжёлых сборок без альтернатив. Если сборка ARM-варианта занимает 15+ минут через QEMU, а amd64 собирается за 3 — это сигнал перейти на нативный remote builder или разделить pipeline.

  6. Раздельные 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 окупается, когда образ действительно должен работать на разных платформах.

Обсуждение в Telegram

Присоединиться →

Комментарии