«Помилка формату виконання.» Найчесніше повідомлення, яке може видати ваш контейнер. Це еквівалент того, як стояти перед дверима з написом натиснути і тягнути на себе: ви випустили бінарник для не того CPU.
Multi-arch має робити це нудним: опублікував одну мітку — працює скрізь. У реальних продакшн-пайплайнах легко випадково опублікувати образ лише для amd64 під тегом, який із задоволенням витягнуть ваші вузли arm64. Або ще гірше: опублікувати список маніфестів, який стверджує, що arm64 існує, тоді як шари всередині насправді amd64. Ось тоді ви отримуєте пейджинг о 02:17 із повідомленням «Kubernetes знову зламався». Насправді ні — зламана ваша збірка.
Що насправді означає «multi-arch» (а чого це не означає)
Multi-arch у Docker — це не магія. Це трюк пакування: один тег образу вказує на список маніфестів (також «індекс»), який містить по одному маніфесту образу для кожної платформи. Реєстр видає правильний маніфест залежно від запиту клієнта щодо платформи.
Ось і все. Реєстр не перевіряє ваші бінарні файли. Docker не читає заголовки ELF і не каже «хмм, підозріло». Якщо ви запушите маніфест arm64, який посилається на amd64-шар, ви успішно опублікуєте неправду. Рантайм скаржитиметься лише тоді, коли ядро відмовиться виконувати цей бінарник.
Ключові терміни, які варто перестати плутати
- Платформа: ОС + архітектура (+ опціональний варіант). Приклад:
linux/amd64,linux/arm64,linux/arm/v7. - Маніфест: JSON, що описує конкретний образ для однієї платформи: конфіг + шари.
- Список маніфестів / індекс: JSON, який відображає платформи на маніфести. Саме це має стояти за вашим тегом, коли ви говорите «multi-arch».
- BuildKit: Двигун збірки, на якому базується
docker buildx. Він виконує важку роботу: крос-збірки, кешування, експорт і віддалені билдери. - QEMU-емуляція: Спосіб запускати ненативні бінарники під час збірки. Зручно, повільніше і іноді з прихованими проблемами.
Якщо ви запам’ятаєте одну річ: multi-arch — це про публікацію коректних метаданих і коректних байтів. Потрібні обидва.
Короткі факти та історія, яка має значення
- «Списки маніфестів» у Docker з’явилися значно пізніше, ніж популярні Docker-образи. На початку Docker не мав першокласної історії для multi-arch; люди використовували окремі теги на кшталт
:armі:amd64і сподівалися, що ніхто не помилиться. - Специфікація OCI стандартизувала те, що зберігають реєстри. Більшість сучасних реєстрів зберігають OCI-сумісні маніфести, навіть якщо ви називаєте їх «Docker-образами». Форма JSON добре визначена; саме інструменти відрізняються.
- BuildKit став значним архітектурним зрушенням. Класичний
docker buildбув прив’язаний до демонa і не був розрахований на масштабні крос-платформені збірки. BuildKit зробив збірки більш паралельними, кешованими та експортованими. - Перевага arm64 у серверах змінила модель загроз. Раніше це були хобі-плати. Тепер це масові інстанси в хмарі. Тягнути amd64-only образ на arm64-вузол вже не «рідкість».
- Kubernetes для multi-arch нічим не відрізняється. Він покладається на ту саму логіку узгодження маніфестів реєстру, що й Docker. Якщо ваш тег неправильний, Kubernetes сумлінно витягне неправильне швидше, ніж ви встигнете сказати «rollout restart».
- Різниця між musl в Alpine і glibc у Debian болісно відчутна під емуляцією. QEMU може маскувати невідповідності ABI до рантайму, і тоді ви отримаєте креші, що виглядають як баги в додатку.
- «Variant» важливий для деяких ARM-цілей.
linux/arm/v7протиlinux/arm/v6— це не дрібниця. Якщо ви випустите неправильний варіант, бінар може виконуватися і все одно падати дивними способами. - Реєстри загалом не валідують коректність платформи. Вони зберігають те, що ви пушите. Ставтеся до реєстру як до надійного сховища, а не як до засобу контролю якості.
Multi-arch достатньо зрілий, щоб бути нудним — якщо ви будуєте його як SRE, який уже отримував опіки.
Як ви опиняєтесь у ситуації з неправильними бінарниками
1) Ви запушили одноархітектурний образ під «універсальним» тегом
Хтось виконав docker build -t myapp:latest . на amd64-ноутбуку і запушив його. Ваші вузли arm64 тягнуть :latest, отримують amd64-шари і падають з exec format error.
Це класика. Воно все ще трапляється у 2026, бо у людей є пальці.
2) Ви збирали multi-arch, але фактично вдало завершилася лише одна платформа
Buildx може збирати кілька платформ однією командою. Також він може мовчки віддати часткові результати, якщо ви не суворі щодо помилок у CI і не перевіряєте список маніфестів після завершення.
3) Ваш Dockerfile «допомагає» тим, що завантажує неправильний попередньо зібраний бінарник
Найпоширеніша форма відправки неправильних архітектур — це не компіляція. Це curl. Dockerfile, який робить:
curl -L -o tool.tgz ...linux-amd64...незалежно від платформи- або використовує інсталятор-скрипт, що за замовчуванням вибирає amd64
- або припускає, що
uname -mвсередині контейнера під час збірки відповідає цільовій архітектурі
Під емуляцією uname -m іноді повертає очікуване, поки не почне вести себе інакше. Під крос-компіляцією воно може показувати архітектуру середовища збірки, а не цільову.
4) Ви покладалися на QEMU для «воно має працювати» і отримали «воно іноді працює»
QEMU корисний для прогресу. Це не те саме, що нативне виконання. Деякі екосистеми мов виконують детекцію архітектури під час збірки. Під емуляцією детекція може бути неправильною, повільною або нестійкою.
5) Ваш кеш змішав архітектури, і ви цього не помітили
Кеші збірки адресуються по вмісту, але логіка вашої збірки може викликати перехресне забруднення архітектур, якщо ви записуєте у загальні шляхи, повторно використовуєте артефакти між стадіями або отримуєте «latest» без фіксації версій на архітектуру.
6) Ваші CI-ранери мультиархітектурні, а припущення команди — одноархітектурні
Коли у вашому флоті є amd64 і arm64 ранери, «збирати там, де доступно» стає «відправляти те, що зібрали». Multi-arch вимагає явності: платформи, походження і верифікація.
Цитата, яку варто тримати в голові під час роботи: Надія — не стратегія.
(перефразована ідея, часто приписується інженерам та операторам у колах надійності)
Жарт №1: Multi-arch образи як перехідники для живлення — все виглядає сумісним, поки ви не вставите у розетку.
Швидкий план діагностики
Коли щось падає в проді, елегантні теорії не дають бонусів. Бали даються за відновлення сервісу та запобігання повторення. Ось найшвидший шлях, який я знаю.
Перше: доведіть, що саме було витягнуто
- Перевірте архітектуру вузла. Якщо вузол arm64, а образ — тільки amd64, припиніть пошуки одразу.
- Проінспектуйте список маніфестів тега. Чи включає тег ту платформу, яку ви вважаєте?
- З’ясуйте фактичний digest, який був витягнутий. Теги змінюються. Digest — ні. Знайдіть digest, який використовує рантайм.
Друге: валідуйте байти всередині образу
- Запустіть debug-контейнер і перевірте бінарник за допомогою
file. Якщо він каже x86-64 на arm64-цілі — ви знайшли палаючий слід. - Перевірте очікування динамічного лінкера / libc. Неправильна архітектура очевидна. Невідповідність ABI може бути підступнішою.
Третє: простежте джерело збірки
- Перегляньте Dockerfile на предмет завантажень. Все, що отримує попередньо зібрані артефакти, має бути чутливим до платформи.
- Перевірте логи buildx для кроків по платформах. Один зелений бейдж збігу може ховати часткову збірку.
- Верифікуйте налаштування QEMU/binfmt, якщо залучена емуляція. Якщо ваша збірка залежить від емуляції, ставте її як залежність з перевірками здоров’я.
Найпоширеніша проблема
Причина не в BuildKit. Причина — відсутність валідації в пайплайні. Збірка «спрацювала» і опублікувала поламаний тег. Система зробила саме те, що ви їй сказали, і в цьому проблема.
Практичні завдання: команди, виводи, рішення
Це не «іграшкові» команди. Це те, що ви запускаєте під час інциденту, і те, що автоматизуєте потім, щоб таких інцидентів не було.
Завдання 1: Підтвердіть архітектуру хоста (не вгадуйте)
cr0x@server:~$ uname -m
aarch64
Що це означає: Вузол — ARM 64-bit. Він очікує образи linux/arm64.
Рішення: Якщо тег образу не рекламує linux/arm64, ви вже в зоні «неправильний бінарник».
Завдання 2: Подивіться, які платформи декларує ваш тег
cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:latest
Name: myorg/myapp:latest
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:8c9c2f7b4f8a5b0d5c0a2b1e9c3d1a6e2f4b7a9c0d1e2f3a4b5c6d7e8f9a0b1c
Manifests:
Name: myorg/myapp:latest@sha256:111...
Platform: linux/amd64
Name: myorg/myapp:latest@sha256:222...
Platform: linux/arm64
Що це означає: Тег — multi-arch індекс, що містить amd64 і arm64 варіанти.
Рішення: Якщо ваша платформа відсутня тут — спочатку виправляйте публікацію. Якщо вона є, переходьте до перевірки вмісту arm64-образу.
Завдання 3: Перегляньте список маніфестів через Docker CLI (альтернативний вигляд)
cr0x@server:~$ docker manifest inspect myorg/myapp:latest | head -n 20
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.index.v1+json",
"manifests": [
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:111...",
"platform": {
"architecture": "amd64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:222...",
Що це означає: Ви дивитесь сире відображення платформ.
Рішення: Коли дебагуєте дивну поведінку реєстру, JSON не брешуть. Якщо список маніфестів не містить вашу платформу, не звинувачуйте Kubernetes.
Завдання 4: Примусово витягніть конкретну платформу локально
cr0x@server:~$ docker pull --platform=linux/arm64 myorg/myapp:latest
latest: Pulling from myorg/myapp
Digest: sha256:222...
Status: Downloaded newer image for myorg/myapp:latest
Що це означає: Ви попросили arm64-варіант і отримали конкретний digest.
Рішення: Використовуйте цей digest для глибшої інспекції. Якщо pull завершується помилкою «no matching manifest», ваша multi-arch публікація неповна.
Завдання 5: Підтвердіть метадані архітектури в конфігу образу
cr0x@server:~$ docker image inspect myorg/myapp:latest --format '{{.Architecture}} {{.Os}}'
arm64 linux
Що це означає: Локальні метадані образу заявляють, що він arm64.
Рішення: Добра ознака, але це не доказ. Далі перевірте реальні виконувані файли.
Завдання 6: Перевірте архітектуру бінарника всередині контейнера
cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:latest -c 'file /usr/local/bin/myapp'
/usr/local/bin/myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, stripped
Що це означає: Байти дійсно arm64.
Рішення: Якщо написано «x86-64», у вас неправильний бінарник на етапі збірки (зазвичай логіка завантаження). Виправляйте Dockerfile, а не реєстр.
Завдання 7: Безпечно відтворіть підпис помилки («exec format error»)
cr0x@server:~$ docker run --rm --platform=linux/arm64 myorg/myapp:latest /usr/local/bin/myapp --version
myapp 2.7.1
Що це означає: Arm64-варіант виконується і виводить версію.
Рішення: Якщо ви отримуєте exec format error, рантайм намагається виконати неправильну архітектуру (або файл не є виконуваним для цієї ОС/арх). Поверніться до Завдання 6.
Завдання 8: Перевірте, яким билдером ви користуєтесь (і чи він адекватний)
cr0x@server:~$ docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default docker
default unix:///var/run/docker.sock running v0.12.5 linux/amd64
multiarch docker-container
multiarch0 unix:///var/run/docker.sock running v0.12.5 linux/amd64,linux/arm64,linux/arm/v7
Що це означає: «default» билдeр може робити лише amd64; docker-container билдeр підтримує кілька платформ (ймовірно через binfmt/QEMU).
Рішення: Використовуйте окремий інстанс билдера для multi-arch. Не довіряйте «default», якщо не полюбляєте несподіванки.
Завдання 9: Створіть і виберіть правильний multi-arch билдeр
cr0x@server:~$ docker buildx create --name multiarch --driver docker-container --use
multiarch
Що це означає: Buildx запустить BuildKit-контейнер для послідовної поведінки й підтримки multi-platform функцій.
Рішення: У CI завжди створюйте/використовуйте іменований билдeр. Це робить збірки відтворюваними й дебажними.
Завдання 10: Перевірте реєстрацію binfmt/QEMU на хості
cr0x@server:~$ docker run --privileged --rm tonistiigi/binfmt --info
Supported platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/arm/v6
Enabled platforms: linux/amd64, linux/arm64, linux/arm/v7
Що це означає: Ядро має обробники binfmt для вказаних архітектур.
Рішення: Якщо цільова платформа не увімкнена, multi-arch збірки, що вимагають емуляції, проваляться або мовчки пропустяться. Увімкніть платформу або переходьте на нативні билдeри для кожної архітектури.
Завдання 11: Зберіть і запуште multi-arch образ (правильний спосіб)
cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 -t myorg/myapp:2.7.1 --push .
[+] Building 142.6s (24/24) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 2.12kB 0.0s
=> [linux/amd64] exporting to image 8.1s
=> => pushing layers 6.7s
=> [linux/arm64] exporting to image 9.4s
=> => pushing layers 7.5s
=> exporting manifest list 1.2s
=> => pushing manifest list 0.6s
Що це означає: Ви зібрали обидві платформи і запушили список маніфестів.
Рішення: Якщо ви не бачите «exporting manifest list», можливо, ви не пушите multi-arch індекс. Виправте це перед тим, як оголосити перемогу.
Завдання 12: Перевірте, що запушений тег є індексом, а не одиночним маніфестом
cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:2.7.1 | sed -n '1,20p'
Name: myorg/myapp:2.7.1
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:4aa...
Manifests:
Name: myorg/myapp:2.7.1@sha256:aaa...
Platform: linux/amd64
Name: myorg/myapp:2.7.1@sha256:bbb...
Platform: linux/arm64
Що це означає: Реєстр тепер зберігає справжній multi-arch індекс для цього тега.
Рішення: Прив’яжіть свій пайплайн до цієї перевірки. Якщо це не індекс — падайте збірку.
Завдання 13: Діагностуйте Dockerfile, який завантажує неправильний артефакт
cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache .
#10 [linux/arm64 6/9] RUN curl -fsSL -o /usr/local/bin/helm.tgz "https://example/helm-linux-amd64.tgz"
#10 0.9s curl: (22) The requested URL returned error: 404
Що це означає: Ваш Dockerfile хардкодить amd64 (і на arm64 це падає, що милосердно). Часто 404 не трапляється і все «працює», при цьому ви відправляєте неправильні байти.
Рішення: Замініть хардкод архітектури на аргументи BuildKit (TARGETARCH, TARGETOS, TARGETVARIANT) і зіставляйте їх з неймінгом постачальника.
Завдання 14: Правильно використовуйте аргументи платформи від BuildKit
cr0x@server:~$ docker buildx build --platform=linux/arm64 --progress=plain --no-cache -t test/myapp:arm64 .
#5 [linux/arm64 3/7] RUN echo "TARGETOS=$TARGETOS TARGETARCH=$TARGETARCH TARGETVARIANT=$TARGETVARIANT"
#5 0.1s TARGETOS=linux TARGETARCH=arm64 TARGETVARIANT=
Що це означає: BuildKit підставляє значення цільової платформи; базуйте завантаження/компіляцію на цих значеннях, а не на uname.
Рішення: Якщо ваш Dockerfile використовує uname -m для вибору бінарів — ви на крок від проблеми. Перейдіть на аргументи BuildKit.
Завдання 15: Підтвердіть платформу запущеного контейнера в Kubernetes (з вузла)
cr0x@server:~$ kubectl get node -o wide | head -n 3
NAME STATUS ROLES AGE VERSION INTERNAL-IP OS-IMAGE KERNEL-VERSION
worker-arm01 Ready <none> 55d v1.29.1 10.0.3.21 Ubuntu 22.04.3 LTS 5.15.0-91-generic
worker-x86a01 Ready <none> 55d v1.29.1 10.0.2.14 Ubuntu 22.04.3 LTS 5.15.0-91-generic
Що це означає: У вас змішаний кластер (тепер це поширено). Планувальник розміщуватиме поди на будь-якому, якщо нема обмежень.
Рішення: Якщо образ не справжній multi-arch, тимчасово прив’яжіть селектори/афініті до вузлів, інакше ви отримаєте російську рулетку «працює на одному вузлі».
Завдання 16: Визначте digest образу, що фактично використовується подом
cr0x@server:~$ kubectl get pod myapp-5f7f6d9c7b-2qk4p -o jsonpath='{.status.containerStatuses[0].imageID}{"\n"}'
docker-pullable://myorg/myapp@sha256:bbb...
Що це означає: Под резолвив тег у digest (незмінний). Цей digest відповідає одному маніфесту платформи.
Рішення: Порівняйте цей digest зі списком маніфестів. Якщо він вказує на неправильний запис платформи — у вас проблема з метаданими реєстру або узгодженням платформи клієнтом.
Завдання 17: Дебаг кешу BuildKit (і припиніть «оптимізувати» сліпо)
cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 \
--cache-to=type=registry,ref=myorg/myapp:buildcache,mode=max \
--cache-from=type=registry,ref=myorg/myapp:buildcache \
-t myorg/myapp:ci-test --push .
[+] Building 98.3s (24/24) FINISHED
=> importing cache manifest from myorg/myapp:buildcache 2.4s
=> [linux/amd64] CACHED 0.6s
=> [linux/arm64] CACHED 0.8s
=> exporting manifest list 1.1s
Що це означає: Кеш було повторно використано для обох платформ, і експортовано список маніфестів.
Рішення: Якщо ви бачите одну платформу в кеші, а інша щоразу збирається заново, порядок Dockerfile або умовні кроки для платформи руйнують повторне використання кешу.
Завдання 18: Ловіть «одноархітектурність під маскою multi-arch» у CI
cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:ci-test | grep -E 'MediaType|Platform'
MediaType: application/vnd.oci.image.index.v1+json
Platform: linux/amd64
Platform: linux/arm64
Що це означає: Ваш тег — індекс і містить обидві платформи.
Рішення: Зробіть це обов’язковим кроком пайплайну. Якщо він друкує application/vnd.oci.image.manifest.v1+json — падайте задачу.
Стратегія збірки, що працює в CI
Правильна стратегія залежить від того, що ви збираєте. Скомпільовані мови поводяться інакше, ніж образи, що просто завантажують бінарник через curl. Але принципи послідовні:
Принцип 1: Віддавайте перевагу нативним збіркам для кожної архітектури, коли можете
Якщо можете запускати arm64-ранер і amd64-ранер — користуйтеся цим. Нативні збірки уникають сюрпризів емуляції і зазвичай швидші для важких компіляцій.
Це не означає, що вам потрібно дві повністю окремі пайплайни. Це означає, що варто продумати топологію билдера:
- Віддалені билдeри для кожної архітектури (BuildKit підтримує такий шаблон)
- Або окремі CI-завдання, які пушать пер-арх образи, а потім публікують список маніфестів
Принцип 2: Якщо використовуєте QEMU — ставтеся до нього як до продакшн-інфраструктури
QEMU через binfmt — вже не «зручність для розробника», якщо воно в CI. Воно може ламатися при оновленнях ядра, Docker або через політики безпеки. Моніторьте його і валідуйте.
Принцип 3: Зробіть Dockerfile чутливим до платформи без зайвої кмітливості
Використовуйте аргументи BuildKit. Вони існують не просто так:
TARGETOS,TARGETARCH,TARGETVARIANTBUILDOS,BUILDARCHдля середовища збірки
Потім явно зіставляйте з неймінгом постачальника. Багато upstream використовують x86_64 замість amd64 або aarch64 замість arm64. Не вгадуйте; зіставляйте.
Принцип 4: Розділяйте «завантажити інструменти» і «збудувати додаток»
Чим більше ви змішуєте обов’язки, тим більше ризикуєте забруднити кеш і переплутати платформи. Чистий шаблон:
- Стадія A: отримання або збірка платформоспецифічних інструментів (ключується по цільовій архітектурі)
- Стадія B: збірка вашого додатка (крос-компіляція, якщо доречно)
- Стадія C: мінімальний runtime-образ (копіюйте лише необхідне)
Принцип 5: Завжди перевіряйте опублікований тег
Валідація дешева. Інциденти дорогі. Після пушу інспектуйте список маніфестів, витягніть кожний варіант платформи і перевірте головний бінарник за допомогою file. Автоматизуйте це.
Жарт №2: Якщо ви не перевіряєте multi-arch, ви фактично граєте в крос-платформну лотерею, де приз — це аутедж.
Три корпоративні міні-історії (анонімізовано, болісно знайомо)
Міні-історія 1: Інцидент через неправильне припущення
Середня компанія перевела частину своїх Kubernetes-вузлів на arm64, щоб знизити витрати. Платформна команда зробила розумно: позначила нові вузли, перемістила туди низькоризикові сервіси і спостерігала метрики. Тиждень виглядав спокійним.
Потім чергова перевидання «нудного» внутрішнього API почало падати тільки на arm-вузлах. На чергуванні бачили crash-loop з exec format error. Вони припустили, що проблема в базовому образі і відкатили апгрейд кластера, який щойно провели. Нічого не змінилося. Відкатали додаток. Все ще не працювало на arm.
Неправильне припущення було простим: «Ми використовуємо buildx, отже наші образи multi-arch». Вони дійсно використовували buildx. Але пайплайн пушив тег із одною командою docker build, що запускалася на amd64-ранері. Інша задача збирала arm64 для тестів, але ніколи його не пушила.
Виправлення було також простим: зробити публікацію manifest list єдиним шляхом до :latest і релізних тегів, і захищати пайплайн перевіркою через imagetools inspect плюс перевіркою бінарника. Глибша мораль — жоден тег не повинен існувати без кроку перевірки, навіть якщо команда «знає», як це працює.
Міні-історія 2: Оптимізація, що відгукнулась бумерангом
Інша організація мала повільну збірку і вирішила «прискорити» шляхом агресивного кешування та повторного використання спільної директорії артефактів між збірками. Вони монтували кеш-том у билдeр і зберігали скомпільовані виходи, ключовані лише по SHA коміту. Це суттєво скоротило час збірки — деякий час.
Потім arm64-образи почали падати з сегфолтами у криптобібліотеці під час TLS-руху. Не відразу. Лише під навантаженням. X86-образи були в порядку. Всі підозрювали баг компілятора, оновлення ядра, космічні промені.
Корінь був прозаїчним: спільна кеш-директорія містила amd64-збірки, які копіювалися у arm64-стадію, бо скрипти збірки використовували «якщо файл існує — повторно використай». Під QEMU деякі кроки збірки пропускалися і кеш короткозамикав компіляцію у найгірший спосіб. Метадані образу все ще заявляли arm64, але байти були змішані.
Вони виправили це, зробивши кеші скопованими за платформою і відмовившись від шарінгу непрозорих директорій артефактів між платформами. Registry-backed cache BuildKit вирішив первісну проблему без небезпечної оптимізації. «Оптимізація» породила проблему постачання архітектур усередині їхнього пайплайну.
Міні-історія 3: Нудна, але правильна практика, що врятувала день
Компанія, пов’язана з фінансами, мала суворий контроль змін і трохи дратівливий процес релізу. Інженери скаржилися на зайві кроки: публікувати образи по digest, записувати маніфести і вести маленький «лог доказів релізу» для кожного деплою. Це здавалося паперовою тяганиною. Але це дозволяло їхнім інженерам на чергуванні більше спати.
Під час навантаженого тижня сервіс почав падати тільки на підмножині вузлів після рутинного ребілду образу. Цього разу реакція на інцидент була майже нудною. Вони взяли digest пода imageID, зіставили його зі списком маніфестів і одразу побачили, що arm64-digest посилається на шари, зібрані два дні тому. amd64-digest був новий.
Пайплайн збірки частково не зміг запушити arm64-шари через тимчасову проблему з дозволами реєстру. Задача все одно опублікувала тег. Але оскільки команда завжди деплоїть по digest у проді і завжди фіксує відповідність тег → digest → платформа, вони швидко змогли зафіксувати відомо-робочий digest для обох платформ і відновити сервіс, поки фіксили CI.
«Нудна» практика — просування по digest і верифікація маніфестів — не запобігла помилці. Вона зробила зону пошкоджень маленькою й діагностику швидкою. Ось справжній виграш.
Поширені помилки: симптом → причина → виправлення
1) Симптом: exec format error при старті контейнера
Корінь: Неправильний бінарник архітектури в образі або тег вказує на маніфест іншої платформи.
Виправлення: Інспектуйте тег через docker buildx imagetools inspect. Витягніть потрібну платформу через docker pull --platform=.... Підтвердіть архітектуру бінарника командою file. Виправляйте завантаження в Dockerfile, використовуючи TARGETARCH.
2) Симптом: Працює на amd64 вузлах, падає на arm64 вузлах, але без exec format error
Корінь: Змішані користувацькі компоненти (плагіни, спільні бібліотеки) або невідповідність ABI (musl vs glibc), часто через скрипти «download latest».
Виправлення: Перевірте file для кожного скопійованого бінарника, не лише головного. Фіксуйте базові образи і версії інструментів. Віддавайте перевагу пакетам дистрибутиву або збірці з джерел для кожної архітектури.
3) Симптом: Тег декларує підтримку arm64, але pull для arm64 каже «no matching manifest»
Корінь: Ви запушили одиночний маніфест, а не список маніфестів; або перезаписали тег одноархітектурним пушем.
Виправлення: Дозволяйте публікацію тега лише через docker buildx build --platform ... --push. Використовуйте права CI, щоб заборонити ручні пуші до релізних тегів.
4) Симптом: Multi-arch збірки надто повільні
Корінь: Емуляція QEMU для важкої компіляції; відсутність кешу; Dockerfile часто інвалідовує кеш.
Виправлення: Перейдіть на нативні билдeри по архітектурі або крос-компілюйте там, де це доречно. Додайте реєстр-кеш. Переставте інструкції Dockerfile для максимального використання кешу.
5) Симптом: arm64 збірка «пройшла», але рантайм не знаходить лоадер
Корінь: Ви скопіювали динамічно лінкований бінар, зібраний проти glibc, у musl-образ (або навпаки), або скопіювали лише бінар без необхідних спільних бібліотек.
Виправлення: Використовуйте одну сім’ю базових образів у всіх стадіях, або збирайте статично, де це доречно. Перевіряйте за допомогою ldd (якщо доступно) або інспектуйте file і шлях інтерпретатора.
6) Симптом: CI зелений, але прод тягне старий digest для однієї архітектури
Корінь: Частковий пуш; список маніфестів оновлено некоректно; крок пушу для однієї платформи впав, інший пройшов.
Виправлення: Заблокуйте реліз на валідації маніфестів і вимагайте наявності обох платформ і свіжої збірки для кожної. Розгляньте пуш пер-арх тегів, а потім створення списку маніфестів явно.
7) Симптом: Docker Compose запускає неправильну архітектуру локально
Корінь: Локальне середовище має платформу за замовчуванням; Compose може будувати локально для хост-архітектури, якщо не вказати platform: або не використовувати buildx коректно.
Виправлення: Встановіть явний platform у Compose для тестування або витягніть з --platform. Не сприймайте поведінку Compose як доказ коректності тегу в реєстрі.
Чеклісти / покроковий план
Покроково: зміцнити multi-arch релізний пайплайн
- Явно визначте цільові платформи. Для більшості бекенд-сервісів:
linux/amd64іlinux/arm64. Додавайтеlinux/arm/v7лише якщо дійсно підтримуєте його. - Створіть іменований билдeр у CI. Використовуйте драйвер
docker-containerдля послідовної поведінки BuildKit. - Встановіть/перевірте binfmt, якщо покладаєтесь на емуляцію. Запустіть команду binfmt info і впевніться, що цільові платформи увімкнені.
- Зробіть Dockerfile чутливим до платформи. Замість
uname -mвикористовуйте мапінг наTARGETARCH. - Збирайте й пуште multi-arch в одній операції. Використовуйте
docker buildx build --platform=... --push. - Перевірте, що опублікований тег є індексом. Падайте збірку, якщо MediaType не є OCI індексом.
- Перевірте головний бінарник кожного варіанта платформи. Витягніть per-platform образи і запустіть
fileдля ентрипойнта. - Просувайте по digest у продакшн. Деплойте незмінні digest; теги залишайте для людей.
- Моніторьте платформні розбіжності. У змішаних кластерах спостерігайте crash-loopи, скорельовані з архітектурою вузла.
- Обмежте, хто може пушити релізні теги. Забороніть «швидкі правки», що обходять валідацію.
Чекліст: рев’ю Dockerfile для multi-arch коректності
- Є
curl/wgetзавантаження? Якщо так — чи URL змінюється залежно відTARGETARCH? - Є інсталятор-скрипти? Чи підтримують вони arm64 явно або за замовчуванням вибирають amd64?
- Копіюються скомпільовані артефакти з іншої стадії? Чи будуються стадії для тієї самої
--platform? - Використовується
unameабоdpkg --print-architecture? Ви впевнені, що вони опитують ціль, а не середовище збірки? - Фіксуєте версії і контрольні суми по архітектурі? Якщо ні — ви довіряєте інтернету в стерео.
Чекліст: ворота верифікації релізу (мінімально прийнятна безпека)
imagetools inspectпоказує OCI індекс і включає всі потрібні платформи.- Пер-платформні pull-и успішні.
- Пер-платформний контейнер виконує
--version(або легку перевірку здоров’я). - Інспекція бінарника через
fileвідповідає очікуваній архітектурі.
Питання й відповіді
1) Чи завжди мені потрібен QEMU для збірки multi-arch образів?
Ні. Якщо ваша збірка — чиста крос-компіляція (наприклад, Go з правильними налаштуваннями), ви можете збирати для інших архітектур без запуску чужих бінарів. QEMU потрібен, коли під час збірки виконуються бінарники цільової архітектури.
2) У чому різниця між docker build і docker buildx build у цьому контексті?
buildx використовує можливості BuildKit: multi-platform виводи, розширені експортери кешу і віддалені билдeри. Класичний docker build зазвичай одноплатформовий і прив’язаний до архітектури локального демона.
3) Чому тег показує підтримку arm64, але бінарник все одно amd64?
Тому що маніфести — це метадані. Ви можете опублікувати маніфест, який каже «arm64», тоді як шар містить amd64-бінарник. Зазвичай так відбувається через хардкодні завантаження або крос-архітектурне забруднення кешу.
4) Чи збирати обидві архітектури в одному джобі або в окремих?
Якщо у вас є надійні нативні ранери для кожної архітектури, окремі джоби можуть бути швидшими і детермінованішими. Якщо ви покладаєтесь на QEMU, один виклик buildx простіший, але може бути повільнішим. У будь-якому разі — публікуйте один список маніфестів і валідуйте його.
5) Чи можу я «виправити» існуючий тег, що неправильний?
Ви можете перепушити тег, щоб він вказував на виправлений список маніфестів, але кеші та розгортання могли вже витягнути поламаний digest. У проді краще деплоїти по digest, щоб виправлення було явною операцією, а не сюрпризом від тега.
6) Чому Kubernetes іноді тягне неправильну архітектуру?
Зазвичай — не тягне. Він витягує те, що надає маніфестне узгодження для платформи вузла. Коли він «тягне неправильно», це часто через те, що тег не був справжнім multi-arch індексом або запис індексу посилається на неправильний вміст.
7) Щодо linux/arm/v7 — чи варто його підтримувати?
Тільки якщо у вас справді є користувачі на 32-бітному ARM. Підтримка ускладнює збірки, розширює матрицю тестування і підвищує ризик помилок варіантів. Не додавайте його просто як трофей.
8) Як зробити «завантажені інструменти» безпечними для різних архітектур?
Використовуйте мапінг на основі TARGETARCH, фіксуйте версії та контрольні суми для кожної архітектури. Краще — використовувати пакети дистрибутиву або збирати з джерел, де це практично.
9) Чи нормально використовувати :latest для multi-arch?
Для зручності розробника — так. Для контракту в проді — ні. Використовуйте незмінні digest або версіоновані теги для релізів, а :latest розглядайте як рухому вказівку.
10) Який єдиний найкращий запобіжний крок, щоб уникнути неправильних бінарів?
Після пушу запустіть роботу верифікації, яка (a) підтверджує, що тег є OCI індексом з потрібними платформами, і (b) перевіряє архітектуру бінарника entrypoint за допомогою file для кожної платформи.
Висновок: наступні кроки, які ви можете зробити цього тижня
Якщо ви управляєте змішаними флотами архітектур — або незабаром керуватимете — multi-arch це не опціональна прикраса. Це базова гігієна релізів. Реєстр охоче зберігає ваші помилки, а Kubernetes охоче їх розгортає в масштабі.
- Додайте ворота верифікації: після пушу вимагайте, щоб
docker buildx imagetools inspectпоказував OCI індекс з усіма платформами. - Перевіряйте байти: для кожної платформи витягніть образ і запустіть
fileна головному виконуваному файлі. - Виправте завантаження в Dockerfile: замініть хардкодні amd64-артефакти на мапінг через
TARGETARCH. - Виберіть стратегію билдера: нативні билдeри по архітектурі, якщо можете; QEMU — якщо потрібно, але тоді моніторьте його як залежність.
- Деплойте по digest: робіть розгортання явними, оборотними і зручними для налагодження.
Правильне multi-arch — це тиша. Це і є мета: без героїчних вчинків, без загадкових крашів, без «працює на моєму вузлі». Просто правильні бінарники щоразу.