Docker «exec format error»: образи не тієї архітектури й як це по-справжньому виправити

Було корисно?

Docker «exec format error»: образи не тієї архітектури й як це по-справжньо виправити

Ви розгортаєте контейнер. На вашому ноутбуці він працював. У стейджингу теж працював. А потім у проді з’являється: exec format error.
Логи порожні, под перезапускається, а канал інцидентів заповнюється тим самим питанням, поставленим різними шрифтами: «Що змінилося?»

Зазвичай: нічого «не змінилося». Ви просто попросили CPU виконати інструкції, призначені для іншого CPU. Контейнери — не магія.
Це пакування. І пакування все ще залежить від архітектури.

Що насправді означає «exec format error»

exec format error — це повідомлення рівня ядра. Linux намагався виконати файл і не впізнав його як виконуваний
бінарний файл для поточної машини. Це не прояв примх Docker. Це операційна система-господар відмовляється завантажувати програму.

У світі контейнерів найпоширеніші причини:

  • Невірна архітектура: ви завантажили образ arm64 на вузол amd64, або навпаки.
  • Невірний формат бінарника всередині образу: база — amd64, але під час збірки ви скопіювали arm64-бінарник.
  • Поганий shebang або CRLF у скрипті entrypoint: ядро не може розібрати рядок інтерпретатора або знаходить невидимі Windows-символи.
  • Відсутній динамічний лоадер: ви збирали під glibc, але запакували в Alpine (musl) середовище без очікуваного лоадера.

Але загальне повідомлення про помилку однакове, саме тому команди годинами сперечаються про «Docker vs Kubernetes vs CI»,
коли ядро вже сказало вам справжнє: «Я не можу це виконати».

Один корисний для операційного мислення образ: образ контейнера — це tarball з файлами плюс трохи метаданих. Коли контейнер стартує,
ядро хоста все одно виконує entrypoint. Якщо цей entrypoint (або інтерпретатор, на який він посилається) не відповідає очікуванням CPU
та ABI хоста, ядро повертає ENOEXEC, а ваш рантайм перетворює це на рядок у логах, над яким ви будете прищурюватися під час інциденту.

Швидкий план діагностики

Коли ви на виклику, вам не потрібна лекція. Потрібна послідовність, що швидко сходиться до відповіді. Ось порядок, який зазвичай дає результати
за хвилини, а не години.

1) Визначте архітектуру вузла (не робіть припущень)

Спочатку перевірте, чим насправді є хост. «Це x86» — часто фольклор, а фольклор — не сигнали моніторингу.

2) Визначте архітектуру образу, який був завантажений

Підтвердіть метадані локального образу: OS/Arch і чи походить він із manifest list.

3) Підтвердіть, який файл не вдається виконати

Знайдіть entrypoint і команду, потім перевірте тип цього файлу всередині образу. Якщо це скрипт, перевірте кінці рядків і shebang.
Якщо бінарник — перевірте заголовки ELF і архітектуру.

4) Рішення: перебудувати vs вибрати платформу vs увімкнути емуляцію

Ієрархія виправлень у продакшні:

  1. Найкраще: опублікувати правильний багатоплатформений образ і розгорнути заново.
  2. Прийнятний терміновий обхід: зафіксувати --platform під час pull/run або в Kubernetes scheduling (якщо ви точно знаєте, що робите).
  3. Крайній засіб: запуск через емуляцію QEMU. Для розробки може бути прийнятно; рідко це вирішення без втрат продуктивності в проді.

Цікаві факти та історія для постмортемів

  • Факт 1: «Exec format error» старший за контейнери; це класична Unix/Linux-помилка, яку повертає ядро, коли не може завантажити формат бінарника.
  • Факт 2: Історія Docker з багатоплатформністю спочатку була важкою; «manifest lists» стали звичним механізмом, щоб один тег посилався на кілька архітектур.
  • Факт 3: Переїзд Apple на ARM (M1/M2/M3) значно збільшив інциденти через невідповідність архітектур, бо ноутбуки розробників перестали відповідати багатьом продакшн-серверам.
  • Факт 4: Kubernetes не «виправляє» невідповідність архітектури; він планує поди на вузли, а вузли виконують те, що їм дали. Невідповідність проявляється як CrashLoopBackOff.
  • Факт 5: Емуляція користувацького режиму QEMU через binfmt_misc робить «запустити ARM-образи на x86» можливим, але це емулювання з реальними накладними витратами й нюансами.
  • Факт 6: Alpine Linux використовує musl libc; Debian/Ubuntu зазвичай — glibc. Відправка бінарника, зв’язаного з glibc, в середовище musl може виглядати як «exec format error» або «no such file».
  • Факт 7: Заголовок ELF у бінарнику містить інформацію про архітектуру. Ви часто можете діагностувати невідповідність миттєво за допомогою file всередині образу.
  • Факт 8: «Працює на моїй машині» отримало новий варіант: «працює на моїй архітектурі». Речення довше, але провина та сама.

Де підкрадаються образи неправильної архітектури

Сценарій A: збірка на ARM-ноутбуках без multi-arch виходу

Розробник збирає образ на Apple Silicon-ноутбуці. Образ має linux/arm64. Він пушить його в реєстр під тегом, який використовує CI.
У проді (x86_64) entrypoint-бінарник — ARM. Бум: exec format error.

Сценарій B: ранери CI тихо змінили архітектуру

Ви перейшли з self-hosted x86-ранерів на керовані ранери, «швидші й дешевші». Сюрприз: пул тепер включає ARM-ранери.
Ваш pipeline детерміністичний — просто не в тому сенсі, у якому ви цього хотіли.

Сценарій C: багатоступенева Dockerfile скопіювала невірний бінарник

Multi-stage збірки чудові. Вони також дуже добре копіюють невірні артефакти.
Якщо перший етап збирається на одній платформі, а другий — на іншій, у вас може опинитися невідповідний бінарник всередині інакше правильного базового образу.

Сценарій D: entrypoint-скрипт виглядає виконуваним, але не є

Entrypoint — shell-скрипт, закомічений з Windows CRLF-кінцями рядків.
Linux намагається його виконати, інтерпретує /bin/sh^M як інтерпретатор і ви отримуєте помилку, що схожа на проблему архітектури.

Сценарій E: тег вказує на одиночний архітектурний образ, а не на manifest list

Команди думають «ми публікуємо multi-arch». Насправді ні. Вони публікують окремі теги.
Потім хтось використовує тег «latest» в проді і отримує ту платформу, яку перезаписав останній build.

Жарт №1: Контейнери як кавомашини в офісі: вони виглядають стандартизованими, поки не виявиться, що половина будівлі працює на несумісних подах.

Практичні завдання: команди, очікуваний вивід і рішення

Цей розділ навмисне операційний. Кожне завдання має три частини: команда, що означає вивід, і яке рішення ви приймаєте далі.
Запускайте їх з вузла, де сталася відмова, або з робочої станції з доступом до образу.

Завдання 1: Підтвердіть архітектуру хоста (Linux)

cr0x@server:~$ uname -m
x86_64

Значення: CPU хоста — x86_64 (amd64). Якщо ви бачите aarch64, це ARM64.

Рішення: Якщо хост — x86_64, а ваш образ — arm64, у вас невідповідність. Продовжуйте, щоб це довести, а потім виправляйте процес збірки/публікації.

Завдання 2: Підтвердіть архітектуру через метадані ОС (більш явно)

cr0x@server:~$ dpkg --print-architecture
amd64

Значення: Назва архітектури в сімействі Debian. Це допомагає, коли скрипти або конфігурація говорять дистро-термінами.

Рішення: Зіставте amd64x86_64, arm64aarch64. Якщо це не відповідає платформі образу — ви знаєте, що далі робити.

Завдання 3: Проінспектуйте платформу образу, який був завантажений

cr0x@server:~$ docker image inspect --format '{{.Os}}/{{.Architecture}} {{.Id}}' myapp:prod
linux/arm64 sha256:5f9c8a0b6c0f...

Значення: Локальний образ — linux/arm64. На вузлі amd64 він не виконуватиметься без емуляції.

Рішення: Або явно завантажте потрібну платформу (терміново), або перебудуйте/опублікуйте багатоплатформений образ (чисте рішення).

Завдання 4: Перегляньте manifest list (на що насправді вказує тег)

cr0x@server:~$ docker manifest inspect myorg/myapp:prod | sed -n '1,60p'
{
   "schemaVersion": 2,
   "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
   "manifests": [
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1784,
         "digest": "sha256:9a1d...",
         "platform": {
            "architecture": "amd64",
            "os": "linux"
         }
      },
      {
         "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
         "size": 1784,
         "digest": "sha256:ab22...",
         "platform": {
            "architecture": "arm64",
            "os": "linux"
         }
      }
   ]
}

Значення: Цей тег — multi-arch manifest list. Це добре. Якщо ви бачите лише один маніфест (без списку), це single-arch.

Рішення: Якщо список містить архітектуру вашого вузла, реєстр в порядку; можливо, ваш pull був примусовим на іншу платформу або рантайм поводиться дивно.
Якщо потрібної платформи там немає — треба опублікувати відсутню.

Завдання 5: Явно завантажити правильну платформу (безпечний терміновий крок)

cr0x@server:~$ docker pull --platform=linux/amd64 myorg/myapp:prod
prod: Pulling from myorg/myapp
Digest: sha256:9a1d...
Status: Downloaded newer image for myorg/myapp:prod

Значення: Ви отримали amd64-варіант. Digest має співпадати з digest-ом amd64-маніфесту зі списку маніфестів.

Рішення: Якщо це спрацювало, чисте рішення — зробити так, щоб тег резолвився автоматично без ручного фіксування платформи.

Завдання 6: Перезапустити контейнер з явною платформою (діагностика)

cr0x@server:~$ docker run --rm --platform=linux/amd64 myorg/myapp:prod --version
myapp 2.8.1

Значення: Образ запускається, коли платформа правильна. Це сильно вказує на невідповідність платформи.

Рішення: Припиніть дебагати сам додаток. Виправляйте публікацію образів та вибір платформи при розгортанні.

Завдання 7: Знайдіть налаштований entrypoint і команду

cr0x@server:~$ docker image inspect --format 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' myorg/myapp:prod
Entrypoint=["/usr/local/bin/entrypoint.sh"] Cmd=["myapp","serve"]

Значення: Невдало виконуваний файл, ймовірно, /usr/local/bin/entrypoint.sh (або те, що тут видно).

Рішення: Перевірте цей файл всередині образу. Не здогадуйтеся.

Завдання 8: Перевірте тип файлу entrypoint всередині образу

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'ls -l /usr/local/bin/entrypoint.sh; file /usr/local/bin/entrypoint.sh'
-rwxr-xr-x 1 root root 812 Jan  2 10:11 /usr/local/bin/entrypoint.sh
/usr/local/bin/entrypoint.sh: POSIX shell script, ASCII text executable, with CRLF line terminators

Значення: CRLF-термінатори рядків — червоний прапорець. Ядро може зламатися на рядку інтерпретатора.

Рішення: Перетворіть на LF у репозиторії або під час збірки. Якщо це бінарник, file скаже вам архітектуру.

Завдання 9: Перевірте архітектуру ELF бінарника всередині образу

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'file /usr/local/bin/myapp; readelf -h /usr/local/bin/myapp | sed -n "1,25p"'
/usr/local/bin/myapp: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, not stripped
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Type:                              EXEC (Executable file)
  Machine:                           AArch64

Значення: Цей бінарник — ARM64. Якщо хост — amd64, це ваш курящий пістолет.

Рішення: Перебудуйте бінарник для правильної платформи або публікуйте multi-arch збірки. Не «виправляйте» це зміною entrypoint.

Завдання 10: Виявлення пастки «відсутній динамічний лоадер»

cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:prod -lc 'ls -l /lib64/ld-linux-x86-64.so.2 /lib/ld-musl-x86_64.so.1 2>/dev/null || true; ldd /usr/local/bin/myapp || true'
ldd: /usr/local/bin/myapp: No such file or directory

Значення: Коли ldd каже «No such file» для файлу, який явно існує, часто це означає, що шлях інтерпретатора (динамічного лоадера) в ELF-заголовку відсутній в образі.
Це невідповідність ABI/базового образу, а не відсутній файл.

Рішення: Переконайтеся, що ваш рантайм-образ відповідає очікуванням libc (glibc vs musl), або відправляйте статично зібраний бінарник, де це доречно.

Завдання 11: Перевірте архітектуру та ОС вузла Kubernetes

cr0x@server:~$ kubectl get nodes -o wide
NAME              STATUS   ROLES    AGE   VERSION   INTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
prod-node-a-01    Ready    worker   92d   v1.28.5   10.0.4.21     Ubuntu 22.04.3 LTS   5.15.0-91-generic  containerd://1.7.11

Значення: Це саме по собі замало. Потрібна ще архітектура.

Рішення: Далі опитайте мітки вузла; Kubernetes кодує arch як мітку.

Завдання 12: Підтвердіть мітку архітектури вузла Kubernetes

cr0x@server:~$ kubectl get node prod-node-a-01 -o jsonpath='{.metadata.labels.kubernetes\.io/arch}{"\n"}{.metadata.labels.kubernetes\.io/os}{"\n"}'
amd64
linux

Значення: Вузол — amd64. Якщо образ — лише arm64, поди зламаються або не стартують.

Рішення: Або плануйте на відповідні вузли (nodeSelector/affinity), або публікуйте потрібний варіант образу. Краще — публікувати.

Завдання 13: Перевірте подові події помилок для підказок про exec/CrashLoop

cr0x@server:~$ kubectl describe pod myapp-7d6c7b9cf4-kkp2l | sed -n '1,120p'
Name:         myapp-7d6c7b9cf4-kkp2l
Namespace:    prod
Containers:
  myapp:
    Image:      myorg/myapp:prod
    State:      Waiting
      Reason:   CrashLoopBackOff
Events:
  Type     Reason     Age                    From               Message
  ----     ------     ----                   ----               -------
  Normal   Pulled     3m12s                  kubelet            Successfully pulled image "myorg/myapp:prod"
  Warning  BackOff    2m41s (x7 over 3m10s)  kubelet            Back-off restarting failed container

Значення: Kubelet успішно потягнув образ; контейнер помирає після спроб запуску. Це сумісно з exec format error.
Вам все ще потрібні логи контейнера.

Рішення: Заберіть логи контейнера (включно з попередніми) і перевірте повідомлення рантайму.

Завдання 14: Здобудьте попередні логи контейнера (помилка часто тут)

cr0x@server:~$ kubectl logs myapp-7d6c7b9cf4-kkp2l -c myapp --previous
exec /usr/local/bin/myapp: exec format error

Значення: Це відмова ядра, з’явлена через рантайм.

Рішення: Підтвердіть архітектуру образу та архітектуру вузла, потім переходьте до публікації правильного manifest list.

Завдання 15: Для вузлів на containerd інспектуйте платформу образу (якщо маєте доступ)

cr0x@server:~$ sudo crictl inspecti myorg/myapp:prod | sed -n '1,80p'
{
  "status": {
    "repoTags": [
      "myorg/myapp:prod"
    ],
    "repoDigests": [
      "myorg/myapp@sha256:9a1d..."
    ],
    "image": {
      "spec": {
        "annotations": {
          "org.opencontainers.image.ref.name": "myorg/myapp:prod"
        }
      }
    }
  }
}

Значення: Вивід crictl залежить від рантайму й конфігурації; не всі налаштування показують платформу тут.
Digest усе ще корисний: його можна зіставити з записом у маніфесті й побачити, яка платформа була обрана.

Рішення: Якщо тут не видно платформу, покладайтесь на інспекцію маніфесту плюс мітки вузла.

Завдання 16: Переконайтеся, що BuildKit/buildx активні (вони вам потрібні)

cr0x@server:~$ docker buildx version
github.com/docker/buildx v0.12.1 3b6e3c5

Значення: Buildx встановлено. Це сучасний шлях для багатоплатформених збірок.

Рішення: Якщо buildx відсутній, встановіть/увімкніть його в CI. Припиніть латати multi-arch вручну скриптами.

Чисте рішення: збірка й публікація багатоплатформених образів

Чисте рішення — нудне: збирайте для платформ, на яких працюєте, публікуйте manifest list і дозвольте клієнтам автоматично тягнути правильний варіант.
Для цього і служать теги. Один тег. Кілька архітектур. Жодних сюрпризів.

Як виглядає «чисто» на практиці

  • Один тег (наприклад, myorg/myapp:prod) вказує на manifest list, що містить linux/amd64 та linux/arm64.
  • Кожен платформений образ збирається з того самого ревізіону коду, з відтворюваними кроками збірки.
  • CI примусово перевіряє, що опублікований тег дійсно містить потрібні платформи.
  • Рантайм не потребує явних --platform перевизначень.

Збірка та пуш multi-arch за допомогою buildx

На машині з Docker BuildKit і buildx ви можете зібрати і запушити в один крок:

cr0x@server:~$ docker buildx create --use --name multiarch
multiarch
cr0x@server:~$ docker buildx inspect --bootstrap | sed -n '1,120p'
Name:          multiarch
Driver:        docker-container
Nodes:
Name:      multiarch0
Endpoint:  unix:///var/run/docker.sock
Status:    running
Platforms: linux/amd64, linux/arm64, linux/arm/v7

Значення: Ваш билд-датчик підтримує кілька платформ. Якщо linux/arm64 не в списку, ймовірно потрібно налаштувати binfmt/QEMU.

Тепер збираємо й пушимо:

cr0x@server:~$ docker buildx build --platform=linux/amd64,linux/arm64 -t myorg/myapp:prod --push .
[+] Building 128.4s (24/24) FINISHED
 => [internal] load build definition from Dockerfile
 => => transferring dockerfile: 2.12kB
 => exporting manifest list myorg/myapp:prod
 => => pushing manifest for myorg/myapp:prod

Значення: Рядок логу «exporting manifest list» — те, чого ви хочете. Це багатоплатформений тег.

Рішення: Якщо виходить лише один маніфест, ви не робили multi-arch. Перевірте платформи билдера й середовище CI.

Коли потрібен binfmt/QEMU (а коли ні)

Якщо ваша збірка на amd64, а ви будуєте arm64-образи (або навпаки), buildx може використовувати емуляцію через binfmt_misc.
Але якщо можна збирати нативно на кожній архітектурі (окремі ранери), це зазвичай швидше й менш ненадійно.

cr0x@server:~$ docker run --privileged --rm tonistiigi/binfmt --install arm64,amd64
installing: arm64
installing: amd64

Значення: Це реєструє QEMU-обробники в ядрі, щоб бінарники іншої архітектури могли запускатися під емуляцією.

Рішення: Використовуйте це, щоб увімкнути багатоплатформені збірки на одному ранері. Для продакшн-навантажень не припускайте, що емуляція прийнятна лише тому, що вона стартує.

Multi-stage Dockerfile: тримайте платформи послідовними

Найпоширеніша «підніжка» — multi-stage збірка, де етап збірки і рантайм-етап не узгоджені за платформою, або ви копіюєте попередньо зібраний бінарник,
завантажений із мережі, який за замовчуванням amd64, тоді як ви будуєте arm64.

Зробіть платформу явною і використовуйте build args, що надає BuildKit:

cr0x@server:~$ cat Dockerfile
FROM --platform=$BUILDPLATFORM golang:1.22 AS build
ARG TARGETOS TARGETARCH
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -o /out/myapp ./cmd/myapp

FROM alpine:3.19
COPY --from=build /out/myapp /usr/local/bin/myapp
ENTRYPOINT ["/usr/local/bin/myapp"]

Значення: Білдер запускається на платформі машини, що будує ($BUILDPLATFORM), але вихідний бінарник компілюється для цільової платформи.

Рішення: Віддавайте перевагу цьому шаблону над «скачати бінарник в Dockerfile». Якщо потрібно завантажувати, вибирайте по TARGETARCH.

Зміцнення: охоронні механізми CI, що запобігають повторенню

Інциденти через образи неправильної архітектури рідко бувають технічно складними. Вони організаційно легко повторювані.
Виправити збірку одного разу — не те саме, що запобігти наступному інциденту.

Охоронний механізм 1: Перевіряйте, що маніфест містить потрібні платформи

Після push інспектуйте маніфест і провалюйте pipeline, якщо він не multi-arch (або відсутня потрібна платформа).

cr0x@server:~$ docker manifest inspect myorg/myapp:prod | grep -E '"architecture": "amd64"|"architecture": "arm64"'
            "architecture": "amd64",
            "architecture": "arm64",

Значення: Обидві платформи присутні. Якщо одна відсутня — ваш тег неповний.

Рішення: Провалюйте збірку. Не «попереджуйте і продовжуйте». Попередження — це спосіб запланувати відмову.

Охоронний механізм 2: Записуйте digest образу і деплойте за digest

Теги — вказівники. Вказівники рухаються. Для продакшну деплойте незмінні digest-си, де можливо, особливо для можливості відкату.
Ви все ще можете публікувати тег для людей, але автоматизація має використовувати digest-и.

cr0x@server:~$ docker buildx imagetools inspect myorg/myapp:prod | sed -n '1,60p'
Name:      myorg/myapp:prod
MediaType: application/vnd.docker.distribution.manifest.list.v2+json
Digest:    sha256:4e3f...
Manifests:
  Name:      myorg/myapp@sha256:9a1d...
  Platform:  linux/amd64
  Name:      myorg/myapp@sha256:ab22...
  Platform:  linux/arm64

Значення: У вас є стабільний digest для manifest list і per-arch digest-и нижче.

Рішення: Зберігайте digest списку маніфестів як артефакт розгортання. Це правильна одиниця для multi-arch.

Охоронний механізм 3: Робіть платформу першим класом параметром у збірках

Якщо в pipeline є прихована варіабельність архітектури, рано чи пізно ви відправите невірні бінарники. Зробіть її явною.
Джоби збірки мають декларувати, які платформи будуються та перевіряються.

Охоронний механізм 4: Забороніть ноутбукам розробників пушити production-теги

Якщо ноутбук може пушити :prod, ви рано чи пізно отримаєте інцидент з батарейкою в причинному ланцюжку.
Відділяйте «dev push» від «release push».

Охоронний механізм 5: Валідуйте entrypoint-бінарник усередині зібраного образу

Додайте пост-білд sanity check: запустіть file на головному бінарнику для кожної платформи. Це ловить «копіювали неправильний артефакт», навіть якщо маніфест виглядає правильно.

cr0x@server:~$ docker buildx build --platform=linux/amd64 -t myorg/myapp:test --load .
[+] Building 22.1s (18/18) FINISHED
 => => naming to docker.io/myorg/myapp:test
cr0x@server:~$ docker run --rm --entrypoint /bin/sh myorg/myapp:test -lc 'file /usr/local/bin/myapp'
/usr/local/bin/myapp: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Значення: Бінарник відповідає amd64. Повторіть для arm64 на нативному ранері або за допомогою емуляції, якщо це прийнятно для базової перевірки.

Одне висловлювання, яке має висіти на кожній стіні збірки: Надія — не стратегія. — перефразована думка, часто цитована в операційних кругах.

Три корпоративні міні-історії (анонімізовано, болісно знайомі)

Міні-історія 1: Інцидент через неправильне припущення

Середня компанія запускала більшість продакшн-робочих навантажень на amd64 Kubernetes-вузлах. Маленький, але зростаючий кластер для пакетної обробки використовував вузли arm64,
бо вони були дешевші для необхідного профілю продуктивності. Обидва кластери тягнули з одного реєстру й обидва використовували ті самі теги.

Команда сервісу пушила новий реліз пізнього вечора. Вони збирали на власному CI-ранері, який роками був amd64.
Під час оптимізації витрат платформа CI тихо додала arm64-ранери до пулу. Планувальник почав розміщувати збірки на arm64 для деяких джобів.
Ніхто цього не занотував, бо з їхньої точки зору «це не має значення».

Пайплайн Docker генерував single-arch образ. Коли збірка виконалася на arm64, пушений тег став arm64-only.
Amd64-продакшн-кластер оновився, потягнув тег і почав негайно падати з exec format error.
Тригери спрацювали; відкат не допоміг, бо попередній тег був перезаписаний раніше того дня.

Виправлення було простим: перебудувати для amd64 і запушити знову. Урок — не простий: «архітектура CI-ранерів — частина вашого ланцюга постачання».
Після інциденту вони ввели обов’язкову публікацію multi-arch і перестали дозволяти змінні теги в проді.
Найцінніша зміна була не технічною. Це була можливість: будь-яка команда могла заблокувати реліз, якщо маніфест не був multi-arch.

Міні-історія 2: Оптимізація, що зіграла злий жарт

Платформена команда вирішила прискорити збірки, кешуючи скомпільовані артефакти між виконаннями pipeline. Ідея розумна.
Вони ввели спільний кеш, ключований по репозиторію і гілці, але не по архітектурі. Ось де історія стала дорогою.

Через тиждень розробник на ARM-ноутбуку запустив збірку локально, оновив кеш у рамках «покращення досвіду розробника».
CI підібрав кешований артефакт, скопіював його в фінальний образ і опублікував образ з тегом amd64, але всередині був arm64-бінарник.
Базовий образ був amd64; бінарник — arm64. Ця невідповідність особливо підступна, бо метадані брешуть, а ядро каже правду.

Інцидент був заплутаний. Інспекція образу показувала linux/amd64. Архітектура вузла була amd64. Але entrypoint падав.
Інженери підозрювали пошкоджені шари, погані реєстри і «можливо Kubernetes тягне не те».
Зрештою хтось запустив file всередині контейнера і отримав правду в один рядок.

Вони зберегли кешування, але змінили ключ: архітектура та версія toolchain стали частиною ідентифікатора кеша.
Також додали пост-білд перевірку, що валідує архітектуру бінарника всередині образу. Виграш у продуктивності лишився; сюрприз дорогою більше не був.

Міні-історія 3: Нудна, але правильна практика, що виручила

Регульований підприємець мав звичку, над якою інші компанії глузували: кожен деплой використовував digest-и, а не теги.
Команди скаржилися. Це виглядало незручним у YAML. Це не було «модним». Але це було надзвичайно складно випадково змінити.

Команда додатку відправила нову версію, зібрану на робочій станції розробника під час термінового хотфіксу. Вони запушили тег, який використовувався в стейджингу.
Образ був arm64. Стейджинг був змішаних архітектур і розгортання впало на половині вузлів. Передбачувано.

Продакшн не постраждав. Продакшн посилався на digest manifest list, створений офіційним релізним pipeline.
Мутований тег ніколи не потрапив у шлях розгортання. Хотфікс був неприємним, але локалізованим.
Платформена команда не мусила «заморожувати теги» або грати в whack-a-mole з реєстром. Процес спрацював.

У post-incident огляді підприємство не вихвалялося. Вони просто вказали на правило: «prod deploys only from signed pipeline artifacts by digest».
Ніхто не плескав у долоні. Нічого не зламалося. Оце і є суть.

Типові помилки: симптом → корінь → виправлення

1) Pod CrashLoopBackOff з «exec format error» в логах

Симптом: Контейнер стартує і одразу виходить; kubectl logs --previous показує exec format error.

Корінь: Архітектура образу не відповідає архітектурі вузла, або головний бінарник всередині образу для іншої архітектури.

Виправлення: Опублікуйте multi-arch образ (manifest list) і розгорніть заново; перевірте мітки архітектури вузла і платформи в маніфесті.

2) Docker run не вдається локально на Apple Silicon, але в CI працює

Симптом: Розробник на M1/M2 бачить exec format error при запуску образу, зібраного/завантаженого звідкись ще.

Корінь: Single-arch amd64-образ завантажено на arm64-хост без емуляції або тег вказує лише на amd64.

Виправлення: Тимчасово використовуйте --platform=linux/arm64; довгостроково — публікуйте multi-arch. Якщо ви покладаєтесь на емуляцію, налаштуйте її навмисно і заміряйте наслідки.

3) Image inspect показує amd64, але все одно «exec format error»

Симптом: docker image inspect показує linux/amd64, але старт дає exec format error.

Корінь: Невірний архітектурний бінарник скопійований в образ (помилка multi-stage, кешований артефакт, завантажений бінарник).

Виправлення: Запустіть file на реальному entrypoint-бінарнику всередині образу; виправте крок збірки, що інжектить артефакт.

4) Entrypoint-скрипт падає з exec format error, але «це ж просто скрипт»

Симптом: Entrypoint — shell-скрипт; помилка з’являється при старті.

Корінь: CRLF-кінці рядків або поганий shebang (шлях інтерпретатора недійсний в образі).

Виправлення: Переконайтеся в LF-кінцях; переконайтеся, що #!/bin/sh вказує на існуючий інтерпретатор; запустіть file всередині образу.

5) «No such file or directory» для бінарника, який існує

Симптом: Логи показують no such file or directory для бінарника; ls показує файл існує.

Корінь: Відсутній ELF-інтерпретатор (динамічний лоадер) або невідповідність libc (glibc-бінарник на Alpine/musl).

Виправлення: Використайте сумісний базовий образ (на glibc) або збирайте статично; валідуйте шлях інтерпретатора через readelf -l.

6) Multi-arch тег існує, але вузли все одно тягнуть неправильний варіант

Симптом: Manifest list включає amd64 і arm64, але вузол тягне не той варіант.

Корінь: Платформа примусово задана через --platform, конфіг рантайму або локальна плутанина з образом/тегом у кеші.

Виправлення: Приберіть примусові налаштування платформи; тягніть за digest; очистіть локальні образи на вузлі за потреби; перевірте інспекцією платформу завантаженого образу.

Жарт №2: «Exec format error» — це спосіб ядра сказати: «Це не моя робота», що також мій улюблений спосіб відмовитися від зустрічей.

Чеклісти / покроковий план

Покроково: реакція на інцидент (15–30 хвилин)

  1. Підтвердіть архітектуру проблемних вузлів (uname -m або мітка вузла Kubernetes).
  2. Проінспектуйте образ, який фактично було завантажено на вузол (docker image inspect або еквівалент рантайму).
  3. Проінспектуйте маніфест тега в реєстрі (docker manifest inspect).
  4. Визначте, чи це платформа невідповідності, чи внутрішній бінарник не того архітектурного типу (file всередині образу).
  5. Якщо невідповідність: завантажте правильну платформу явно як тимчасову міру або відкотіться на відомий хороший digest.
  6. Розпочніть чисте виправлення: перебудовуйте й пуште multi-arch manifest list.
  7. Додайте у CI перевірку маніфестів і архітектури бінарників для entrypoint.

Покроково: впровадження чистого рішення (той самий день)

  1. Увімкніть BuildKit/buildx в CI і стандартизуйте на ньому.
  2. Зробіть Dockerfile безпечним для multi-arch: використовуйте $BUILDPLATFORM, $TARGETARCH і компілюйте для цілі.
  3. Збирайте й пуште: docker buildx build --platform=linux/amd64,linux/arm64 ... --push.
  4. Перевірте, що manifest list містить потрібні платформи.
  5. Зробіть smoke test для кожної платформи (нативний ранер кращий; емуляція прийнятна для базової перевірки).
  6. Розгорніть за digest manifest list, а не за змінний тег.

Покроково: запобігання (цей спринт)

  1. Забороніть продакшн-деплойти з використанням змінних тегів; в проді — лише digest-и.
  2. Залиште права реєстру: тільки CI може пушити релізні теги.
  3. Робіть архітектуру ранерів явною в плануванні CI; не дозволяйте «змішані пули» без multi-arch-збірок.
  4. Додайте запис походження артефактів збірки, що включає побудовані платформи та digest manifest list.
  5. Навчіть команду: архітектура — частина інтерфейсу, а не реалізаційна деталь.

Питання й відповіді

Q1: Чи завжди «exec format error» означає проблему з CPU-архітектурою?

Ні, але це перше, що варто перевірити, оскільки це поширено і швидко довести. Скрипти з CRLF-кінцями і поганими shebang-ами також можуть це викликати,
а невідповідність ELF-інтерпретатора може виглядати схоже.

Q2: Чому Docker іноді «просто працює» між архітектурами на моєму ноутбуці?

Бо у вас може бути зареєстрована емуляція QEMU через binfmt_misc, яку часто встановлює Docker Desktop або попередній крок налаштування.
Це зручно. Але це може ховати проблеми, поки ви не дістанетеся до продвузлів без емуляції.

Q3: У чому різниця між образом і manifest list?

Окремий образ — це файловий набір і конфіг для однієї платформи. Manifest list — це індекс, що вказує на кілька platform-specific образів під одним тегом.
Клієнти вибирають правильний образ на основі своєї платформи (якщо не вказано інше).

Q4: У Kubernetes, чи можу я примусити правильну архітектуру за допомогою node selectors?

Так. Ви можете використовувати kubernetes.io/arch у nodeSelector або правилах affinity. Це корисно, коли ви справді запускаєте різні збірки для різних архітектур.
Але це не заміна публікації multi-arch образів, коли застосунок має працювати повсюди.

Q5: Чи варто використовувати --platform у продакшн-розгортаннях?

Лише як тимчасовий обхід або в жорстко контрольованих ситуаціях. Це створює приховану політику, яка може зламати планування і приховати помилку публікації.
Довгострокове рішення — правильні маніфести й збірки.

Q6: Чому ldd іноді каже «No such file» для існуючого бінарника?

Тому що ядро не може завантажити інтерпретатор бінарника (динамічний лоадер), вказаний у ELF-заголовках. Цей шлях відсутній в образі,
часто через невідповідність glibc/musl або відсутні пакети лоадера.

Q7: Чи можемо публікувати окремі теги для кожної архітектури замість multi-arch маніфестів?

Можна, але ви про це пошкодуєте, якщо у вас немає строгої дисципліни іменування, планування і розгортання. Multi-arch маніфести дозволяють одному тегу поводитися коректно
в різних середовищах, що зменшує людські помилки — найпоширеніший ресурс у більшості організацій.

Q8: Який найшвидший доказ, що бінарник зібраний для неправильної архітектури?

Запустіть file на ньому всередині контейнера (або на артефакті перед пакуванням). Він одразу скаже «x86-64» vs «ARM aarch64».
Для більш деталей запустіть readelf -h.

Q9: Якщо ми робимо multi-arch, чи потрібно тестувати на обох архітектурах?

Так. Принаймні smoke test. Multi-arch збірки можуть падати через архітектурно-залежні проблеми: різні залежності, поведінка CGO або доступність нативних бібліотек.
«Скомпільовано» — не те саме, що «запускається».

Висновок: наступні кроки, що приживаються

Коли ви бачите Docker exec format error, ставтеся до цього як до пожежної тривоги продакшна: вона гучна, різка і найчастіше права.
Не починайте з переписування entrypoint-ів або звинувачень на адресу Kubernetes. Почніть з підтвердження архітектури з обох боків і валідації того, що насправді всередині образу.

Практичні наступні кроки:

  • Сьогодні: перевірте платформу проблемного образу і entrypoint-бінарник за допомогою docker image inspect і file.
  • Цього тижня: опублікуйте багатоплатформений manifest list за допомогою buildx і перевірте це в CI.
  • У цьому спринті: розгорніть за digest у продакшні і заблокуйте, хто може пушити релізні теги.

Чисте рішення не хитромудре. Воно правильне. І вартує значно менше, ніж відкриття ще одного разу, що ваші CPU мають власну думку.

← Попередня
Сучасний CSS :has() у реальному UI: селектори батьків для форм, карток, фільтрів
Наступна →
Видалення знімків ZFS: чому вони не видаляються і як це виправити

Залишити коментар