Learning Platform
Глоссарий Troubleshooting
Урок 19.03 · 22 мин
Средний
dockerbuildxmulti-archciarm64

Multi-arch builds: arm64 + amd64

В 2026 году у DE-команды два мира:

  • Локальные машины: Mac M1/M2/M3 (Apple Silicon, arm64), Linux ARM laptops.
  • Production: AWS EC2 / GCP VM / k8s — обычно amd64 (x86_64), но AWS Graviton (arm64) стремительно растёт.

Если ты собираешь образ только на arm64 (потому что собираешь на Mac), он не запустится на amd64-сервере: exec format error. И наоборот. Решение — multi-arch build: один образ в registry, который содержит варианты для обеих архитектур, и Docker сам выбирает нужный при pull.

В этом уроке: зачем multi-arch, как собрать через buildx --platform, разница QEMU emulation vs native runners, как проверить, что pulled правильная arch.


Зачем multi-arch

Сценарий 1: ты на Mac M1 собрал образ:

docker build -t my-etl:v1 .
docker push my-etl:v1

На сервере AWS amd64:

docker run my-etl:v1
# exec /usr/local/bin/python: exec format error

Сценарий 2: твой DAG на Airflow в k8s (amd64 nodes) использует образ, который ты тестировал локально на arm64. В тестах всё работало — в проде не запустилось.

Multi-arch решает обе проблемы. Один tag :v1 в registry содержит оба варианта. При docker pull Docker смотрит на uname -m хоста и скачивает нужный.

Manifest list: один тег = N архитектур
my-etl:v1manifest listManifest list (OCI image index). Не содержит данных сам, ссылается на per-platform manifest'ы.
amd64 manifestamd64 manifest. Слои собраны под x86_64. Скачивается на AWS EC2 / Intel/AMD машинах.
arm64 manifestarm64 manifest. Слои собраны под aarch64. Скачивается на Mac M1+ / AWS Graviton / Raspberry Pi 4+.
M1 clientdocker pull my-etl:v1 на Mac M1. Docker daemon смотрит на uname -m -> aarch64, выбирает arm64 manifest, качает соответствующие слои.
EC2 clientdocker pull my-etl:v1 на EC2 amd64. Качается amd64 manifest. Транспарентно для пользователя.

Buildx: основной инструмент

docker build (legacy builder) не умеет multi-arch. Нужен Buildx — современный builder из BuildKit.

# Проверь, что Buildx есть (в современном Docker -- включён по умолчанию)
docker buildx version
# github.com/docker/buildx v0.26.1 ...

# Список builder'ов
docker buildx ls

# Создай новый builder (если по дефолту не "container")
docker buildx create --name multiarch --use --bootstrap

# Запусти multi-arch build
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/user/my-etl:v1 \
  --push \
  .

Что произошло:

  1. buildx create --name multiarch создал builder типа docker-container — это контейнер с BuildKit внутри (отличается от legacy builder).
  2. --platform linux/amd64,linux/arm64 сказал Buildx собрать ДВА варианта.
  3. --push сразу запушил в registry. Multi-arch builds НЕЛЬЗЯ load в локальный daemon (legacy daemon хранит images per-arch). Только push или save в OCI archive.

QEMU emulation vs native runners

Почему exec format error возникает при запуске образа не той архитектуры

На Mac M1 (arm64) ты можешь собрать amd64-вариант через QEMU emulation. Buildx запустит QEMU, который эмулирует x86_64 инструкции. Это работает, но медленно (3-10x медленнее native).

QEMU emulation (одна машина, две arch)

# Установить binfmt-handlers через qemu-user-static
docker run --privileged --rm tonistiigi/binfmt --install all

# Теперь Buildx может собирать любые platform на amd64 (или arm64) машине
docker buildx build --platform linux/amd64,linux/arm64 -t my-etl:v1 .

На Mac M1 amd64-сборка через QEMU занимает минут 5-15 для image с pip install — потому что каждая команда RUN эмулируется. Для DE-образов с pandas/pyspark это бывает мучительно.

Native multi-arch runners (быстро)

На CI вместо QEMU используют отдельные runner’ы для каждой arch и собирают параллельно. Этот подход:

  • amd64-job собирает amd64-вариант на ubuntu-latest (amd64 runner).
  • arm64-job собирает arm64-вариант на ubuntu-latest-arm (arm64 runner — GitHub предоставляет с 2024).
  • После — merge manifest list через docker manifest.
# .github/workflows/multi-arch.yml
name: multi-arch-build

on:
  push:
    branches: [main]

jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: linux/amd64
            runner: ubuntu-latest
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: ${{ matrix.platform }}
          outputs: |
            type=image,name=ghcr.io/${{ github.repository_owner }}/my-etl,push-by-digest=true,name-canonical=true,push=true
          cache-from: type=gha,scope=${{ matrix.platform }}
          cache-to: type=gha,mode=max,scope=${{ matrix.platform }}
      - name: Export digest
        run: |
          mkdir -p /tmp/digests
          echo "${{ steps.build.outputs.digest }}" > /tmp/digests/$(echo ${{ matrix.platform }} | tr / -)
      - uses: actions/upload-artifact@v4
        with:
          name: digests-${{ strategy.job-index }}
          path: /tmp/digests/*

  merge:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: /tmp/digests
          pattern: digests-*
          merge-multiple: true
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/setup-buildx-action@v3
      - name: Create manifest list
        working-directory: /tmp/digests
        run: |
          docker buildx imagetools create \
            -t ghcr.io/${{ github.repository_owner }}/my-etl:latest \
            $(printf 'ghcr.io/${{ github.repository_owner }}/my-etl@sha256:%s ' $(cat *))

Native runners собирают параллельно, без QEMU. amd64 + arm64 — параллельные jobs (~ 2-3 минуты каждый), потом merge (~ 30 секунд).

TIP

GitHub предоставляет бесплатные ubuntu-24.04-arm runners с 2024 года для public repos. Для private — через GitHub-hosted larger runners (платная подписка) или self-hosted ARM-машины.


Простой вариант: QEMU в одном job

Если у тебя нет доступа к arm-runner’ам, или образ маленький — QEMU достаточен:

# .github/workflows/multi-arch-qemu.yml
name: build-multiarch-qemu

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-qemu-action@v3
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ghcr.io/${{ github.repository_owner }}/my-etl:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

setup-qemu-action устанавливает QEMU. После этого один job собирает оба варианта через emulation. Время сборки: 5-15 минут на medium-image, против 2-3 минут на native.


Как проверить, что pulled правильная arch

После push’а — проверь manifest list:

docker manifest inspect ghcr.io/user/my-etl:v1
{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": { "architecture": "amd64", "os": "linux" },
      "digest": "sha256:abc..."
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "platform": { "architecture": "arm64", "os": "linux" },
      "digest": "sha256:def..."
    }
  ]
}

Видишь две архитектуры — multi-arch собран корректно.

После docker pull:

docker image inspect ghcr.io/user/my-etl:v1 --format '{{.Architecture}}'
# arm64   (на Mac M1)
# amd64   (на EC2)

Или внутри контейнера:

docker run --rm ghcr.io/user/my-etl:v1 uname -m
# aarch64 (на Mac)
# x86_64  (на EC2)
WARNING

ВНИМАНИЕ: если ты на Mac M1 пишешь docker run —platform linux/amd64 my-etl:v1, ты ЗАСТАВИШЬ запуск amd64-варианта через QEMU. Это полезно для тестирования “как будет в prod”, но медленно. Без —platform Docker автоматически выбирает arm64.


Когда multi-arch не нужен

Не каждому DE-образу нужны обе arch. Сценарии:

  • Только prod на amd64, dev в k8s на amd64: тебе не нужен arm64. Собирай только linux/amd64.
  • Локальный compose на Mac, но Spark выполняется в EMR (amd64): для compose-стенда собирай arm64, для spark-submit-кода в EMR — amd64. Это два разных image.
  • Embedded / IoT: только arm64 (или armv7).

Multi-arch — это удобство, не обязательство. Не плати за неё временем CI, если у тебя одна целевая платформа.


Альтернатива: cross-compile

Для Go / Rust / Java можно cross-compile без QEMU:

# Multi-stage с cross-compile
FROM --platform=$BUILDPLATFORM golang:1.23 AS build
ARG TARGETOS TARGETARCH
WORKDIR /app
COPY . .
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/app

FROM gcr.io/distroless/static:nonroot
COPY --from=build /out/app /app
ENTRYPOINT ["/app"]

$BUILDPLATFORM — platform, на которой запущен Buildx (например, amd64 runner). $TARGETPLATFORM / $TARGETOS / $TARGETARCH — целевая платформа.

Сборка:

docker buildx build --platform linux/amd64,linux/arm64 .

Buildx запустит один контейнер на amd64 (нет QEMU), но go build дважды — для каждой целевой arch. Это быстро, потому что Go сам компилирует кросс-arch.

Для Python такое не работает — Python runtime per-arch, не cross-compile. Но если у тебя DE-инструмент на Go (например, vector, telegraf) — это идеальный паттерн.


Попробуй сам

# 1. Подготовь QEMU (один раз)
docker run --privileged --rm tonistiigi/binfmt --install all

# 2. Создай простой Dockerfile
cat > Dockerfile <<EOF
FROM python:3.11-slim
RUN echo "Architecture: \$(uname -m)" > /arch.txt
CMD cat /arch.txt
EOF

# 3. Собери multi-arch локально (не пушим)
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t my-etl:test \
  --output type=oci,dest=image.tar \
  .
# Multi-arch нельзя load в обычный daemon -- сохраняем в OCI archive

# 4. Или: собери и сразу push в registry (тогда увидишь manifest list)
docker login ghcr.io
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t ghcr.io/USERNAME/test-multiarch:v1 \
  --push \
  .

# 5. Проверь manifest
docker manifest inspect ghcr.io/USERNAME/test-multiarch:v1

# 6. На Mac M1 запусти и убедись, что arm64
docker pull ghcr.io/USERNAME/test-multiarch:v1
docker run --rm ghcr.io/USERNAME/test-multiarch:v1
# Architecture: aarch64

# 7. Force amd64 через QEMU
docker run --rm --platform linux/amd64 ghcr.io/USERNAME/test-multiarch:v1
# Architecture: x86_64

Проверка знанийKnowledge check
Ты на Mac M1 собрал образ через docker build -t my-etl:v1 ., запушил на ECR, и k8s в AWS на amd64-nodes падает с "exec format error". Какие два варианта починки правильные и какой быстрее?
ОтветAnswer
Образ собран только под arm64 (потому что docker build на M1 без --platform = arm64). amd64-нода не может запустить arm64-бинарники. Решения: (1) docker buildx build --platform linux/amd64,linux/arm64 -t my-etl:v1 --push . -- собрать multi-arch через QEMU emulation (медленно, 5-15 минут). (2) Перенести build в CI на ubuntu-latest (amd64 runner), собрать только linux/amd64 -- быстрее (~2 минуты). Если на amd64-only prod, вариант (2) правильнее. Для multi-arch dev+prod -- вариант (1), но идеально на CI с native arm-runner ubuntu-24.04-arm + amd64-runner параллельно через matrix.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Зачем DE-команде с Mac M1 разработчиками и amd64 production может потребоваться multi-arch builds?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4