Docker « exec format error » : images de mauvaise architecture et correction propre

Cet article vous a aidé ?

Vous déployez un conteneur. Il fonctionnait sur votre ordinateur portable. Il a même fonctionné en staging. Puis la production renvoie : exec format error.
Les logs sont vides, le pod redémarre, et votre canal d’incident se remplit de la même question posée en différentes polices : « Qu’est-ce qui a changé ? »

En général : rien n’« a changé ». Vous avez juste demandé à un CPU d’exécuter des instructions destinées à un autre CPU. Les conteneurs ne sont pas magiques.
Ce sont des emballages. Et l’emballage tient toujours compte de l’architecture.

Ce que signifie réellement « exec format error »

exec format error est une plainte du noyau. Linux a tenté d’exécuter un fichier et n’a pas pu le reconnaître comme un binaire exécutable
pour la machine courante. Ce n’est pas Docker qui fait une crise. C’est le système hôte qui refuse de charger le programme.

Dans l’univers des conteneurs, les causes les plus courantes sont :

  • Mauvaise architecture : vous avez tiré une image arm64 sur un nœud amd64, ou l’inverse.
  • Mauvais format binaire dans l’image : votre base est amd64 mais vous avez copié un binaire arm64 pendant la construction.
  • Mauvais shebang ou CRLF dans un script d’entrypoint : le noyau ne peut pas analyser la ligne d’interpréteur ou trouve des caractères invisibles Windows.
  • Chargeur dynamique manquant : vous avez compilé pour glibc mais livré un runtime Alpine (musl) sans le chargeur attendu.

Mais le message d’erreur principal est le même, ce qui explique pourquoi les équipes perdent des heures à débattre « Docker vs Kubernetes vs CI »
alors que le noyau vous a déjà dit le vrai problème : « Je ne peux pas exécuter ça. »

Un modèle mental opérationnel utile : une image de conteneur est une archive tar remplie de fichiers plus des métadonnées. Quand le conteneur démarre,
le noyau hôte exécute toujours l’entrypoint. Si cet entrypoint (ou l’interpréteur auquel il réfère) ne correspond pas aux attentes CPU
et ABI de l’hôte, le noyau renvoie ENOEXEC, et votre runtime le convertit en une ligne de log que vous scruterez pendant une panne.

Procédure de diagnostic rapide

Quand vous êtes de garde, vous ne voulez pas une leçon. Vous voulez une séquence qui converge rapidement. Voici l’ordre qui tend à produire des réponses
en minutes plutôt qu’en heures.

1) Identifier l’architecture du nœud (ne présumez pas)

Vérifiez d’abord ce qu’est réellement l’hôte. « C’est du x86 » est souvent du folklore, et le folklore n’est pas un signal de monitoring.

2) Identifier l’architecture de l’image telle qu’elle a été tirée

Confirmez les métadonnées locales de l’image : OS/Arch et si elle provient d’une manifest list.

3) Confirmer quel fichier échoue à s’exécuter

Trouvez l’entrypoint et la commande, puis examinez le type de ce fichier dans l’image. Si c’est un script, vérifiez les fins de ligne et le shebang.
Si c’est un binaire, vérifiez les en-têtes ELF et l’architecture.

4) Décider : reconstruire vs sélectionner la plateforme vs activer l’émulation

Hiérarchie des corrections en production :

  1. Meilleur : publier une image multi-arch correcte et redéployer.
  2. Solution d’urgence acceptable : pinner --platform lors du pull/run ou dans l’ordonnancement Kubernetes (si vous savez ce que vous faites).
  3. Dernier recours : exécuter via l’émulation QEMU. Ça peut convenir pour le dev ; ce n’est rarement une solution neutre en performance en prod.

Faits et historique utiles pour les post-mortems

  • Fait 1 : « Exec format error » est antérieur aux conteneurs ; c’est une erreur Unix/Linux classique renvoyée quand le noyau ne peut pas charger un format binaire.
  • Fait 2 : L’histoire multi-arch de Docker a été initialement cahoteuse ; les « manifest lists » sont devenues le mécanisme courant pour qu’un tag réfère à plusieurs architectures.
  • Fait 3 : Le passage d’Apple à l’ARM (M1/M2/M3) a fortement augmenté les incidents wrong-arch parce que les portables des développeurs ne correspondaient plus à beaucoup de serveurs de production.
  • Fait 4 : Kubernetes ne « corrige » pas les incompatibilités d’architecture ; il planifie les pods sur des nœuds, et ce sont les nœuds qui exécutent ce qu’on leur donne. La mismatch apparaît comme CrashLoopBackOff.
  • Fait 5 : L’émulation user-mode QEMU via binfmt_misc est ce qui rend « exécuter des images ARM sur x86 » possible, mais c’est toujours de l’émulation avec un véritable surcoût et des cas limites.
  • Fait 6 : Alpine Linux utilise musl libc ; Debian/Ubuntu utilisent typiquement glibc. Livrer un binaire lié à glibc dans une image musl peut ressembler à « exec format error » ou « no such file ».
  • Fait 7 : L’en-tête ELF d’un binaire contient l’architecture. Vous pouvez souvent diagnostiquer les mismatches instantanément avec file à l’intérieur de l’image.
  • Fait 8 : « Works on my machine » a une nouvelle variante : « works on my architecture ». La phrase est plus longue, mais la faute est la même.

Où se glissent les images wrong-arch

Scénario A : construction sur des portables ARM sans sortie multi-arch

Un développeur construit une image sur un portable Apple Silicon. L’image est linux/arm64. Il la pousse dans le registre sous un tag utilisé par le CI.
En production (x86_64), le binaire d’entrypoint est ARM. Boum : exec format error.

Scénario B : les runners CI ont changé d’architecture en silence

Vous avez migré de runners self-hosted x86 vers des runners managés « plus rapides et moins chers ». Surprise : la flotte inclut maintenant des runners ARM.
Votre pipeline de build est déterministe — juste pas dans le sens que vous vouliez.

Scénario C : un Dockerfile multi-stage a copié le mauvais binaire

Les builds multi-stage sont formidables. Ils sont aussi très efficaces pour copier le mauvais artefact.
Si l’étape de build tourne sur une plateforme et l’étape finale sur une autre, vous pouvez vous retrouver avec des binaires incompatibles intégrés dans une base autrement correcte.

Scénario D : le script d’entrypoint semble exécutable mais ne l’est pas

L’entrypoint est un script shell commité avec des fins de ligne CRLF de Windows.
Linux tente de l’exécuter, interprète /bin/sh^M comme l’interpréteur, et vous obtenez une erreur qui ressemble fortement à un problème d’architecture.

Scénario E : le tag pointe vers une image mono-arch et non une manifest list

Les équipes pensent « nous publions multi-arch ». Elles ne le font pas. Elles publient des tags séparés.
Puis quelqu’un utilise le tag « latest » en prod et obtient la plateforme que la dernière construction a écrasée.

Blague #1 : Les conteneurs sont comme les machines à café de bureau : elles semblent standardisées jusqu’à ce que vous découvriez que la moitié de l’immeuble tourne sur des pods incompatibles.

Tâches pratiques : commandes, sortie attendue et décisions

Cette section est volontairement opérationnelle. Chaque tâche a trois parties : la commande, ce que signifie la sortie, et quelle décision prendre ensuite.
Exécutez-les depuis un nœud où l’erreur se produit, ou depuis une station de travail ayant accès à l’image.

Task 1: Confirm host architecture (Linux)

cr0x@server:~$ uname -m
x86_64

Signification : Le CPU de l’hôte est x86_64 (amd64). Si vous voyez aarch64, c’est ARM64.

Décision : Si l’hôte est x86_64 et que votre image est arm64, vous avez une incompatibilité. Poursuivez pour le prouver, puis corrigez le processus de build/publication.

Task 2: Confirm architecture via OS metadata (more explicit)

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

Signification : Nom Debian de l’architecture. Utile quand des scripts ou configs parlent en termes de distribution.

Décision : Mappez amd64x86_64, arm64aarch64. Si ces valeurs ne correspondent pas à la plateforme de votre image, vous savez ce qui vient.

Task 3: Inspect the image platform you pulled

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

Signification : L’image locale est linux/arm64. Sur un hôte amd64, elle ne s’exécutera pas sans émulation.

Décision : Soit tirer la bonne plateforme explicitement (court terme) soit reconstruire/publier une image multi-arch (correction propre).

Task 4: Inspect the manifest list (what the tag actually points to)

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"
         }
      }
   ]
}

Signification : Ce tag est une manifest list multi-arch. C’est bon. Si vous ne voyez qu’un seul manifest (pas de liste), c’est mono-arch.

Décision : Si la liste inclut l’architecture de votre nœud, le registre est correct ; votre pull peut être forcé vers une autre plateforme ou votre runtime fait quelque chose d’étrange.
Si elle n’inclut pas l’architecture du nœud, vous devez publier celle qui manque.

Task 5: Pull the correct platform explicitly (safe emergency move)

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

Signification : Vous avez récupéré la variante amd64. Le digest doit correspondre au digest amd64 du manifest list.

Décision : Si cela fonctionne, la correction propre reste de faire en sorte que le tag résolve correctement sans pinning manuel de platform.

Task 6: Re-run the container with explicit platform (diagnostic)

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

Signification : L’image s’exécute quand la plateforme est correcte. Cela implique fortement un mismatch de plateforme.

Décision : Arrêtez de déboguer l’application elle-même. Corrigez la publication et la sélection d’image/déploiement.

Task 7: Find the configured entrypoint and command

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"]

Signification : L’exécutable en échec est probablement /usr/local/bin/entrypoint.sh (ou ce qui apparaît ici).

Décision : Inspectez ce fichier dans l’image. Ne devinez pas.

Task 8: Inspect the entrypoint file type inside the image

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

Signification : Les terminaisons CRLF sont un signal d’alerte. Le noyau peut échouer sur la ligne d’interpréteur.

Décision : Convertissez en LF dans le dépôt ou pendant la build. Si c’est un binaire, file vous indiquera l’architecture.

Task 9: Inspect a binary’s ELF architecture inside the image

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

Signification : Ce binaire est ARM64. Si l’hôte est amd64, c’est l’arme fumante.

Décision : Recompiler le binaire pour la plateforme correcte ou publier des builds multi-arch. Ne « corrigez » pas cela en changeant l’entrypoint.

Task 10: Detect the “missing dynamic loader” trap

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

Signification : ldd rapportant « No such file » pour un fichier qui existe souvent signifie que le chemin de l’interpréteur (chargeur dynamique) dans l’en-tête ELF est absent de l’image.
C’est un mismatch ABI/base-image, pas un binaire manquant.

Décision : Assurez-vous que votre image runtime correspond aux attentes libc (glibc vs musl) ou livrez un binaire statiquement lié si approprié.

Task 11: Check Kubernetes node architecture and OS

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

Signification : Ceci n’est pas suffisant en soi. Vous avez aussi besoin de l’architecture.

Décision : Interrogez ensuite les labels du nœud ; Kubernetes encode l’arch comme label.

Task 12: Confirm Kubernetes node architecture label

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

Signification : Le nœud est amd64. Si l’image est uniquement arm64, les pods échoueront ou ne démarreront jamais.

Décision : Soit planifier sur des nœuds correspondants (nodeSelector/affinity) soit publier la variante d’image correcte. Préférez la publication.

Task 13: Inspect a failing pod events for exec/CrashLoop hints

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

Signification : Kubelet a bien tiré l’image ; le conteneur meurt après les tentatives de démarrage. C’est compatible avec exec format error.
Il vous faut toujours les logs du conteneur.

Décision : Récupérez les logs du conteneur (y compris les précédents) et vérifiez les messages du runtime.

Task 14: Grab the previous container logs (the error often shows here)

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

Signification : C’est le refus du noyau remonté par le runtime.

Décision : Confirmez l’arch de l’image vs l’arch du nœud, puis passez à la publication d’une manifest list correcte.

Task 15: For containerd-based nodes, inspect image platform (if you have access)

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"
        }
      }
    }
  }
}

Signification : La sortie de crictl varie selon le runtime et la config ; toutes les configurations n’exposent pas la plateforme directement ici.
Le digest reste utile : vous pouvez le relier à une entrée de manifest et voir quelle plateforme a été sélectionnée.

Décision : Si vous ne voyez pas la plateforme ici, fiez-vous à l’inspection du manifest plus les labels d’architecture des nœuds.

Task 16: Verify BuildKit/buildx is active (you want it)

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

Signification : Buildx est installé. C’est la voie moderne pour les builds multi-arch.

Décision : Si buildx manque, installez/activez-le dans le CI. Arrêtez d’essayer de bricoler le multi-arch avec des scripts maison.

La correction propre : construire et publier des images multi-arch

La correction propre est ennuyeuse : construisez pour les plateformes que vous exécutez, publiez une manifest list, et laissez les clients tirer la variante correcte automatiquement.
C’est à cela que servent les tags. Un tag. Plusieurs architectures. Zéro surprise.

À quoi ressemble une « correction propre » en pratique

  • Un seul tag (par ex., myorg/myapp:prod) pointe vers une manifest list incluant linux/amd64 et linux/arm64.
  • Chaque image par plateforme est construite depuis la même révision source, avec des étapes de build reproductibles.
  • Le CI impose que le tag publié contienne réellement les plateformes requises.
  • Le runtime ne nécessite pas d’overrides --platform.

Construire et pousser multi-arch avec buildx

Sur une machine avec Docker BuildKit et buildx, vous pouvez builder et pousser en une seule commande :

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

Signification : Votre builder prend en charge plusieurs plateformes. Si linux/arm64 n’est pas listé, vous devez probablement configurer binfmt/QEMU.

Maintenant, build et push :

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

Signification : La ligne de log « exporting manifest list » est ce que vous voulez. C’est le tag multi-arch.

Décision : Si cela ne produit qu’un seul manifest, vous n’avez pas réellement buildé multi-arch. Vérifiez les plateformes du builder et l’environnement CI.

Quand vous avez besoin de binfmt/QEMU (et quand vous n’en avez pas besoin)

Si votre machine de build est amd64 et que vous construisez des images arm64 (ou l’inverse), buildx peut utiliser l’émulation via binfmt_misc.
Mais si vous pouvez builder nativement sur chaque architecture (runners séparés), c’est généralement plus rapide et moins fragile.

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

Signification : Cela enregistre les handlers QEMU dans le noyau pour que des binaires d’architecture étrangère puissent tourner sous émulation.

Décision : Utilisez cela pour activer les builds multi-arch sur un seul runner. Pour les charges de production, ne supposez pas que l’émulation est acceptable simplement parce que ça démarre.

Dockerfiles multi-stage : garder les plateformes cohérentes

Le piège le plus courant est un build multi-stage où la phase de build et la phase runtime ne s’alignent pas par plateforme, ou vous copiez un binaire précompilé
téléchargé depuis Internet qui par défaut est amd64 alors que vous buildiez arm64.

Rendez la plateforme explicite et utilisez les build args fournis par 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"]

Signification : Le builder tourne sur la plateforme de la machine de build ($BUILDPLATFORM), mais le binaire produit est compilé pour la plateforme cible.

Décision : Préférez ce modèle plutôt que « télécharger un binaire dans le Dockerfile ». Si vous devez télécharger, sélectionnez selon TARGETARCH.

Durcissement : garde-fous CI pour éviter les récidives

Les incidents causés par des images wrong-arch sont rarement « difficiles ». Ils sont organisationnellement faciles à reproduire.
Corriger le build une fois n’est pas la même chose que prévenir la prochaine occurrence.

Garde-fou 1 : Affirmer que le manifest contient les plateformes requises

Après le push, inspectez le manifest et faites échouer le pipeline s’il n’est pas multi-arch (ou s’il manque une plateforme requise).

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

Signification : Les deux plateformes apparaissent. Si l’une manque, votre tag est incomplet.

Décision : Faites échouer la build. Ne « prévenez et continuez » pas. Les avertissements sont la manière dont les pannes sont programmées.

Garde-fou 2 : Enregistrer le digest de l’image et déployer par digest

Les tags sont des pointeurs. Les pointeurs bougent. Pour la production, déployez des digests immuables quand c’est possible, surtout pour la propreté des rollbacks.
Vous pouvez toujours publier un tag pour les humains, mais laissez l’automatisation utiliser les digests.

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

Signification : Vous avez un digest stable pour la manifest list et des digests par-arch en dessous.

Décision : Stockez le digest de la manifest list comme artefact de déploiement. C’est l’unité correcte pour le multi-arch.

Garde-fou 3 : Faire de la plateforme un paramètre de première classe dans les builds

Si votre pipeline a une variabilité d’architecture cachée, vous finirez par publier les mauvais bits. R rendez cela explicite.
Les jobs de build doivent déclarer quelles plateformes sont construites et validées.

Garde-fou 4 : Arrêter de laisser les portables développeurs publier des tags production

Si un portable peut publier :prod, vous aurez tôt ou tard un incident de production où le niveau de batterie fait partie de la chaîne causale.
Séparez le « dev push » du « release push ».

Garde-fou 5 : Valider le binaire d’entrypoint dans l’image construite

Ajoutez une vérification post-build : lancez file sur le binaire principal pour chaque plateforme. Cela détecte le « binaire injecté par erreur » même quand le manifest semble correct.

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

Signification : Le binaire correspond à amd64. Répétez pour arm64 en utilisant un runner natif ou un contrôle émulé si acceptable.

Une citation qui devrait figurer sur tous les murs de build : Espérer n’est pas une stratégie. — idée paraphrasée souvent utilisée en exploitation.

Trois mini-récits d’entreprise (anonymisés, douloureusement familiers)

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Une entreprise de taille moyenne exécutait la plupart de ses charges de production sur des nœuds Kubernetes amd64. Un petit cluster de traitement par lots utilisait des nœuds arm64
car ils étaient moins chers pour le profil de performance nécessaire. Les deux clusters tiraient du même registre et utilisaient les mêmes tags d’image.

Une équipe de service a poussé une nouvelle version en fin d’après-midi. Ils ont buildé sur leur flotte de runners CI, qui était amd64 depuis des années.
Lors d’un sprint d’optimisation des coûts, le groupe plateforme CI a silencieusement ajouté des runners arm64 à la flotte. Le scheduler a commencé à exécuter des builds sur arm64 pour certains jobs.
Personne ne l’a documenté parce que, de leur point de vue, cela « ne devrait pas avoir d’importance ».

Le pipeline Docker de l’équipe produisait une image mono-arch. Quand le job a tourné sur arm64, le tag poussé est devenu arm64-only.
Le cluster de production amd64 a mis à jour, a tiré le tag, et a commencé à planter instantanément avec exec format error.
Les alertes ont retenti ; le rollback n’a pas aidé parce que le tag précédent avait déjà été écrasé plus tôt dans la journée.

La correction a été simple : rebuild pour amd64 et push à nouveau. La leçon n’était pas simple : « l’architecture des runners CI fait partie de votre chaîne d’approvisionnement ».
Après l’incident, ils ont introduit la publication multi-arch obligatoire, et ils ont arrêté d’autoriser les tags mutables pour les déploiements production.
Le changement le plus précieux n’était pas technique. C’était l’autorisation : toute équipe pouvait bloquer une release si le manifest n’était pas multi-arch.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une équipe plateforme a décidé d’accélérer les builds en mettant en cache les artefacts compilés entre les exécutions de pipeline. Idée raisonnable.
Ils ont introduit un cache partagé indexé par dépôt et branche, pas par architecture. C’est là que l’histoire devient coûteuse.

Une semaine plus tard, un développeur sur un portable ARM a lancé le build localement, poussant une mise à jour de cache dans le cadre d’une amélioration « developer experience ».
Le CI a récupéré l’artefact en cache, l’a copié dans l’image finale, et a publié une image taggée amd64 contenant un binaire arm64 à l’intérieur.
L’image de base était amd64 ; le binaire était arm64. Ce mismatch est un type spécial de malédiction car les métadonnées mentent tandis que le noyau dit la vérité.

L’incident a été déroutant. L’inspection de l’image indiquait linux/amd64. L’architecture du nœud était amd64. Pourtant l’entrypoint échouait.
Les ingénieurs ont passé du temps à soupçonner des couches corrompues, des registres défaillants, et « peut-être que Kubernetes tire la mauvaise chose ».
Finalement quelqu’un a exécuté file à l’intérieur du conteneur et a eu la vérité en une ligne.

Ils ont conservé le caching, mais ont corrigé la clé : l’architecture et la version de la toolchain sont devenues partie de l’identité du cache.
Ils ont aussi ajouté une vérification post-build qui validait l’architecture du binaire à l’intérieur de l’image. Le gain de performance est resté ; le coût surprise a disparu.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une entreprise régulée avait une habitude dont se moquent d’autres sociétés : chaque déploiement utilisait des digests, pas des tags.
Les équipes se plaignaient. Cela paraissait peu convivial dans le YAML. Ce n’était pas « moderne ». C’était, cependant, extrêmement difficile à muter accidentellement.

Une équipe applicative a livré une nouvelle version construite sur un poste de développeur lors d’un hotfix urgent. Ils ont poussé un tag utilisé par le staging.
L’image était arm64. Le staging était mixte-arch, et le rollout a échoué sur la moitié des nœuds. Prévisible.

La production s’en fichait. La production référençait un digest de manifest list produit par le pipeline de release officiel.
Le tag muté n’est jamais entré dans le chemin de déploiement. Le hotfix a été pénible, mais contenu.
L’équipe plateforme n’a pas eu à « geler les tags » ni jouer au whac-a-mole du registre. Le processus a fait son boulot.

Dans le post-mortem, l’entreprise ne s’est pas vantée. Ils ont juste pointé la règle : « les deploys prod seulement depuis des artefacts pipeline signés par digest ».
Personne n’a applaudi. Rien n’a cassé. C’est le but.

Erreurs courantes : symptôme → cause racine → fix

1) Pod CrashLoopBackOff avec « exec format error » dans les logs

Symptôme : Le conteneur démarre et quitte immédiatement ; kubectl logs --previous montre exec format error.

Cause racine : L’architecture de l’image ne correspond pas à l’architecture du nœud, ou le binaire principal dans l’image est pour la mauvaise architecture.

Correctif : Publier une image multi-arch (manifest list) et redéployer ; vérifier les labels d’arch des nœuds et les plateformes du manifest.

2) Docker run échoue localement sur Apple Silicon mais fonctionne en CI

Symptôme : Un développeur sur M1/M2 voit exec format error en exécutant une image construite/tirée ailleurs.

Cause racine : Image mono-arch amd64 tirée sur un hôte arm64 sans émulation, ou le tag pointe uniquement vers amd64.

Correctif : Utiliser temporairement --platform=linux/arm64 ; à long terme publier multi-arch. Si vous comptez sur l’émulation, configurez-la intentionnellement et mesurez.

3) Image inspect dit amd64, pourtant « exec format error »

Symptôme : docker image inspect montre linux/amd64, mais le démarrage échoue avec exec format error.

Cause racine : Mauvais binaire copié dans l’image (mix-up multi-stage, artefact en cache, binaire téléchargé).

Correctif : Lancez file sur le véritable binaire d’entrypoint à l’intérieur de l’image ; corrigez l’étape de build qui injecte l’artefact.

4) Le script d’entrypoint échoue avec exec format error, alors que ce n’est « que » un script

Symptôme : L’entrypoint est un script shell ; l’erreur apparaît au démarrage.

Cause racine : Fins de ligne CRLF ou mauvais shebang (chemin d’interpréteur invalide dans l’image).

Correctif : Assurer des terminaisons LF ; vérifier que #!/bin/sh pointe vers un interpréteur existant ; exécuter file dans l’image.

5) « No such file or directory » pour un binaire qui existe

Symptôme : Les logs montrent no such file or directory pour le binaire ; ls montre que le fichier existe.

Cause racine : Chargeur ELF (interpréteur dynamique) manquant ou mismatch libc (binaire glibc sur Alpine/musl).

Correctif : Utiliser une image de base compatible (glibc) ou build statique ; valider le chemin de l’interpréteur via readelf -l.

6) Tag multi-arch existe, mais les nœuds tirent toujours la mauvaise variante

Symptôme : La manifest list inclut amd64 et arm64, pourtant un nœud tire la mauvaise.

Cause racine : La plateforme est forcée via --platform, config runtime, ou confusion d’images locales/en cache.

Correctif : Supprimez les settings de plateforme forcée ; tirez par digest ; videz les images locales sur le nœud si nécessaire ; vérifiez en inspectant la plateforme de l’image tirée.

Blague #2 : « Exec format error » est la façon qu’a le noyau de dire « Ce n’est pas mon travail », ce qui est aussi ma manière préférée de décliner des réunions.

Listes de contrôle / plan pas-à-pas

Pas-à-pas : réponse à incident (15–30 minutes)

  1. Confirmer l’architecture des nœuds défaillants (uname -m ou label Kubernetes).
  2. Inspecter l’image réellement tirée sur le nœud (docker image inspect ou équivalent runtime).
  3. Inspecter le manifest du tag dans le registre (docker manifest inspect).
  4. Déterminer si c’est un mismatch de plateforme ou un mismatch binaire interne (file dans l’image).
  5. En cas de mismatch : tirer la bonne plateforme explicitement comme mitigation courte, ou rollback vers un digest connu-bon.
  6. Commencer la correction propre : rebuild et push d’une manifest list multi-arch.
  7. Ajouter une assertion CI qui vérifie les plateformes du manifest et l’architecture binaire de l’entrypoint.

Pas-à-pas : implémentation de la correction propre (même jour)

  1. Activer BuildKit/buildx dans le CI et standardiser dessus.
  2. Rendre le Dockerfile multi-arch-safe : utiliser $BUILDPLATFORM, $TARGETARCH, et compiler pour la cible.
  3. Build et push : docker buildx build --platform=linux/amd64,linux/arm64 ... --push.
  4. Vérifier que la manifest list contient les plateformes requises.
  5. Smoke test par plateforme (runner natif préféré ; l’émulation acceptable pour des vérifs basiques).
  6. Déployer en utilisant le digest de la manifest list, pas un tag mutable.

Pas-à-pas : prévention (ce sprint)

  1. Interdire les déploiements production depuis des tags mutables ; utiliser des digests en prod.
  2. Verrouiller les permissions du registre : seul le CI peut pousser les tags de release.
  3. Rendre l’architecture des runners explicite dans la planification CI ; n’autorisez pas de « mixed pools » sans builds multi-arch.
  4. Ajouter un registre de provenance des artefacts de build qui inclut les plateformes construites et le digest de la manifest list.
  5. Former l’équipe : l’architecture fait partie de l’interface, pas d’un détail d’implémentation.

FAQ

Q1: Est-ce que « exec format error » est toujours un problème d’architecture CPU ?

Non, mais c’est la première chose à vérifier car c’est courant et rapide à prouver. Les scripts avec fins de ligne CRLF et les mauvais shebangs peuvent aussi le déclencher,
et les mismatches d’interpréteur ELF peuvent sembler similaires.

Q2: Pourquoi Docker fonctionne parfois « juste » entre architectures sur mon portable ?

Parce que vous avez peut-être l’émulation QEMU enregistrée via binfmt_misc, souvent installée par Docker Desktop ou une étape précédente.
C’est pratique. Ça peut aussi masquer des problèmes jusqu’à ce que vous rencontriez des nœuds de production sans émulation.

Q3: Quelle est la différence entre une image et une manifest list ?

Une image unique est le filesystem et la config d’une seule plateforme. Une manifest list est un index qui pointe vers plusieurs images spécifiques par plateforme sous un seul tag.
Les clients sélectionnent l’image correcte selon leur plateforme (sauf si c’est forcé autrement).

Q4: Dans Kubernetes, puis-je forcer la bonne architecture avec des sélecteurs de nœud ?

Oui. Vous pouvez utiliser kubernetes.io/arch dans des node selectors ou des règles d’affinité. C’est utile quand vous exécutez effectivement des builds différents par arch.
Mais ce n’est pas un substitut à la publication d’images multi-arch quand l’application doit tourner partout.

Q5: Devrait-on utiliser --platform dans les déploiements production ?

Seulement comme mitigation temporaire ou dans des situations strictement contrôlées. Cela devient une politique cachée qui peut casser les hypothèses de scheduling et masquer une mauvaise publication.
La correction longue durée est des manifests corrects et des builds corrects.

Q6: Pourquoi ldd dit parfois « No such file » pour un binaire existant ?

Parce que le noyau ne peut pas charger l’interpréteur (chargeur dynamique) référencé dans les en-têtes ELF du binaire. Ce chemin de chargeur n’existe pas dans l’image,
souvent dû à un mismatch glibc/musl ou des paquets loader manquants.

Q7: Peut-on publier des tags séparés par architecture au lieu de manifests multi-arch ?

Vous pouvez, mais vous le regretterez à moins d’avoir une discipline stricte de nommage, d’ordonnancement et de déploiement. Les manifests multi-arch permettent à un seul tag de se comporter correctement
dans tous les environnements, ce qui réduit l’erreur humaine — la ressource la plus abondante dans la plupart des organisations.

Q8: Quelle est la preuve la plus rapide qu’un binaire est construit pour la mauvaise arch ?

Exécutez file dessus à l’intérieur du conteneur (ou sur l’artefact avant empaquetage). Il vous dira « x86-64 » vs « ARM aarch64 » immédiatement.
Suivez avec readelf -h si vous avez besoin de plus de détails.

Q9: Si nous construisons multi-arch, faut-il tester sur les deux architectures ?

Oui. Au minimum un smoke test. Les builds multi-arch peuvent échouer de façons spécifiques à l’architecture : dépendances différentes, comportement CGO, ou disponibilité de bibliothèques natives.
« Ça a buildé » n’est pas la même chose que « ça tourne ».

Conclusion : prochaines étapes durables

Quand vous voyez l’erreur Docker exec format error, traitez-la comme une alarme incendie en production : c’est bruyant, direct, et généralement juste.
Ne commencez pas par réécrire des entrypoints ou blâmer Kubernetes. Commencez par confirmer l’architecture des deux côtés et validez ce qu’il y a réellement dans l’image.

Prochaines étapes pratiques :

  • Aujourd’hui : inspectez la plateforme de l’image en échec et le binaire d’entrypoint avec docker image inspect et file.
  • Cette semaine : publiez une manifest list multi-arch en utilisant buildx, et vérifiez-la dans le CI.
  • Ce sprint : déployez par digest en production et verrouillez qui peut pousser les tags de release.

La correction propre n’est pas ingénieuse. Elle est correcte. Et elle coûte bien moins cher que de découvrir, encore une fois, que vos CPUs ont des opinions.

← Précédent
CSS moderne : :has() dans l’UI réelle — sélecteurs parents pour formulaires, cartes et filtres
Suivant →
Suppression des snapshots ZFS : pourquoi ils refusent de disparaître (et comment y remédier)

Laisser un commentaire