Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Changer de socle sans casser l'application

Par mis à jour le

Améliorer cette page

Logo Docker

Changer la ligne FROM d'un Dockerfile remplace la libc de l'image, la bibliothèque C que presque tous les programmes appellent pour lire un fichier, ouvrir une connexion ou réserver de la mémoire. Debian et Ubuntu utilisent la glibc, Alpine utilise musl, et un programme compilé pour l'une peut ne plus démarrer avec l'autre.

Cette leçon s'adresse à qui sait déjà choisir un socle et veut maintenant prévoir ce qu'un changement casse, avant de publier. Vous y apprendrez à lire ce qu'un binaire réclame, à corriger un binaire glibc qui ne démarre plus sur Alpine, à comprendre pourquoi pip n'installe pas les mêmes fichiers d'un socle à l'autre, et à reconnaître l'erreur d'une différence d'architecture. Toutes les mesures viennent de Docker 29.9.2 sur amd64.

  • Lire l'interpréteur ELF d'un binaire et vérifier qu'il existe dans le nouveau socle
  • Corriger un binaire glibc qui ne démarre plus sur Alpine, par trois voies chiffrées
  • Expliquer pourquoi pip choisit une roue manylinux sur Debian et musllinux sur Alpine
  • Repérer un paquet Python sans roue pour le nouveau socle, et une installation silencieusement dégradée
  • Diagnostiquer une différence d'architecture, dont l'erreur n'est pas toujours celle qu'on attend
  • Tester l'application dans le nouveau socle avant de publier

Que change un changement de FROM pour un binaire compilé ?

Section intitulée « Que change un changement de FROM pour un binaire compilé ? »

Un exécutable Linux est un fichier au format ELF. Quand il est dynamique, il ne contient pas la libc : il désigne dans son en-tête un petit programme, son interpréteur (ou chargeur), qui charge les bibliothèques au démarrage. Ce fichier doit exister dans l'image, sinon le binaire ne démarre pas.

La glibc et musl ne le placent pas au même endroit. Le noyau répond alors no such file or directory sur un binaire pourtant présent : l'appel système execve(2) rend l'erreur ENOENT aussi quand c'est l'interpréteur qui manque, et le message cite le fichier demandé, pas l'interpréteur. La leçon choisir et maintenir une image de base montre ce cas en détail. Ici, vous le reprenez pour apprendre à prévoir l'échec, puis à le corriger.

Reprenez le même programme C, cumul.c, qui additionne quatre mesures de pluie. Travaillez dans un dossier dédié, que la section de nettoyage supprime.

Fenêtre de terminal
mkdir -p ~/changer-socle/glibc ~/changer-socle/musl ~/changer-socle/static ~/changer-socle/py && cd ~/changer-socle
cumul.c
#include <stdio.h>
int main(void) {
int pluies[] = {4, 9, 3, 17};
int cumul = 0;
for (int i = 0; i < 4; i++) {
cumul += pluies[i];
}
printf("cumul=%d\n", cumul);
return 0;
}

Enregistrez ce code sous le nom cumul.c dans ~/changer-socle. Vous le compilez dans une image glibc, gcc:14, qui pèse environ 2,14 Go : elle ne sert qu'à produire le binaire. La commande file affiche son interpréteur, ldd ses bibliothèques.

Fenêtre de terminal
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/src -w /src \
gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bd \
sh -c 'gcc -O2 -o glibc/cumul cumul.c && file glibc/cumul && ldd glibc/cumul'
glibc/cumul: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped
linux-vdso.so.1 (0x00007f8451359000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8451157000)
/lib64/ld-linux-x86-64.so.2 (0x00007f845135b000)

Regardez la ligne interpreter /lib64/ld-linux-x86-64.so.2 : c'est le fichier que le noyau doit trouver pour démarrer ce binaire. Les adresses entre parenthèses changent à chaque exécution. Lancez maintenant ce binaire dans chacun des deux socles pour voir la conséquence.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/glibc/cumul
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/glibc/cumul

Sur Debian, le binaire s'exécute et affiche cumul=33 ; sur Alpine, il échoue sur exec /src/glibc/cumul: no such file or directory, avec le code 255 rendu par docker run (le code 127 n'apparaît que si un shell lance le programme).

Comment savoir, avant de changer, si le binaire passera ?

Section intitulée « Comment savoir, avant de changer, si le binaire passera ? »

On le sait en lisant l'interpréteur du binaire dans l'image qui l'a construit, puis en vérifiant que ce chemin existe dans le nouveau socle. Ni file ni readelf ne sont installés dans alpine ou debian:13-slim (sh: file: not found), d'où la lecture dans l'image de compilation. Le chemin lu plus haut se cherche ensuite dans chaque candidat.

Fenêtre de terminal
docker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a \
sh -c 'ls -l /lib/ld-* /lib64 2>&1'
docker run --rm debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 \
sh -c 'ls -l /lib64/ld-* /lib/x86_64-linux-gnu/ld-* 2>&1'
ls: /lib64: No such file or directory
-rwxr-xr-x 1 root root 670312 Oct 21 15:50 /lib/ld-musl-x86_64.so.1
-rwxr-xr-x 1 root root 225672 Jun 29 21:37 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 44 Jun 29 21:37 /lib64/ld-linux-x86-64.so.2 -> ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2

Alpine n'a pas de /lib64 et son chargeur s'appelle /lib/ld-musl-x86_64.so.1 ; Debian fournit /lib64/ld-linux-x86-64.so.2. Le chemin réclamé par le binaire existe donc sur Debian et pas sur Alpine : la lecture a prédit l'échec sans rien lancer.

Parce que le ldd d'Alpine répond sans erreur sur un binaire glibc qui ne peut pas démarrer. Il rend le code 0 et affiche une réponse qui a l'air valide.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a ldd /src/glibc/cumul
/lib64/ld-linux-x86-64.so.2 (0x7f1d81374000)
libc.so.6 => /lib64/ld-linux-x86-64.so.2 (0x7f1d81374000)

Regardez la seconde ligne : libc.so.6 est rattachée à l'interpréteur lui-même, ce qui ne prouve rien. Le ldd de musl ne signale aucune incompatibilité, donc ne l'utilisez pas comme test ; lancez le binaire, ou lisez son interpréteur dans une image glibc comme ci-dessus.

Quelques commandes disent de quelle libc un socle dispose. Elles servent à confirmer, pas à décider : l'arbitre reste le test de fumée, qui consiste à lancer l'application dans l'image candidate (il est détaillé à la fin de cette leçon). Le fichier /etc/os-release nomme la distribution (ID=alpine et VERSION_ID=3.24.3 sur Alpine), pas la libc. Deux commandes se distinguent des autres parce que leur échec est lui-même une information.

Fenêtre de terminal
docker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a getconf GNU_LIBC_VERSION
docker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a ldd --version
docker run --rm debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 getconf GNU_LIBC_VERSION
getconf: GNU_LIBC_VERSION: unknown variable
musl libc (x86_64)
Version 1.2.6
Dynamic Program Loader
Usage: /lib/ld-musl-x86_64.so.1 [options] [--] pathname
glibc 2.41

getconf GNU_LIBC_VERSION répond glibc 2.41 sur Debian et unknown variable (code 1) sur Alpine : l'échec est le signal « ce n'est pas glibc ». ldd --version nomme aussi la libc d'Alpine, mais rend le code 1 parce que musl traite l'option comme une erreur d'usage et écrit la version sur la sortie d'erreur. Un script qui teste le code de sortie de ldd --version conclut donc à tort, sur Alpine, que la commande a échoué, alors que musl répond et nomme sa version.

Que faire quand le binaire glibc ne passe pas sur Alpine ?

Section intitulée « Que faire quand le binaire glibc ne passe pas sur Alpine ? »

Trois corrections existent : recompiler dans le nouveau socle, lier le binaire en statique, ou ajouter une couche de compatibilité. Les deux premières demandent les sources du programme, la troisième sert quand vous ne les avez pas. Elles ne coûtent pas la même chose.

Compilez dans une image Alpine avec le paquet build-base. Le conteneur tourne en root pour installer le paquet ; la commande rend ensuite le fichier à votre compte avec chown, évalué par votre shell avant le lancement.

Fenêtre de terminal
docker run --rm -v "$PWD":/src -w /src \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a \
sh -c "apk add --no-cache build-base 2>&1 | tail -2 && gcc -O2 -o musl/cumul cumul.c && ldd musl/cumul && chown $(id -u):$(id -g) musl/cumul"
Executing busybox-1.37.0-r31.trigger
OK: 275.5 MiB in 38 packages
/lib/ld-musl-x86_64.so.1 (0x7fc37aa29000)
libc.musl-x86_64.so.1 => /lib/ld-musl-x86_64.so.1 (0x7fc37aa29000)

Le binaire musl fait 18 384 octets. Pour l'obtenir, le conteneur a installé 38 paquets (275,5 Mio) en environ 7 s, hors téléchargement de l'image : un compilateur n'a pas sa place dans l'image finale (voir optimiser la taille des images pour la construction en plusieurs étapes). Lancez maintenant ce binaire dans les deux socles : vous allez voir une symétrie.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/musl/cumul
docker run --rm -v "$PWD":/src:ro \
debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/musl/cumul

Sous Alpine, il affiche cumul=33. Sous debian:13-slim, il échoue exactement comme le binaire glibc sous Alpine, sur exec /src/musl/cumul: no such file or directory (code 255). Changer de socle, dans un sens comme dans l'autre, impose de recompiler.

Un lien statique copie les bibliothèques dans le binaire, qui ne réclame plus aucun interpréteur. On l'obtient avec l'option -static de gcc, dans l'image glibc.

Fenêtre de terminal
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/src -w /src \
gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bd \
sh -c 'gcc -O2 -static -o static/cumul cumul.c && file static/cumul && ldd static/cumul'
static/cumul: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, with debug_info, not stripped
not a dynamic executable

ldd répond not a dynamic executable avec le code 1 : ici ce code est attendu. Lancez le binaire dans les deux socles.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/static/cumul
docker run --rm -v "$PWD":/src:ro \
debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/static/cumul

Ce binaire affiche cumul=33 sous Alpine et sous Debian. Son coût est la taille : 934 520 octets contre 15 688 pour la version dynamique, soit environ 60 fois plus pour ce programme minuscule, car il emporte avec lui la libc.

Avec glibc, le lien statique a une limite. Les fonctions qui résolvent des noms, comme getaddrinfo, chargent encore des bibliothèques de la glibc à l'exécution, et l'éditeur de liens avertit que le binaire exige alors celles de la version utilisée à la compilation. Un programme qui résout des noms ou des utilisateurs doit donc être testé sur chaque socle, ou lié contre musl.

gcompat est un paquet Alpine qui apporte un chargeur de remplacement portant le nom de l'interpréteur glibc, plus des fonctions compatibles avec l'interface de la glibc : le noyau trouve le fichier réclamé, et le binaire tourne sur musl. Il convient quand le binaire vient d'un tiers que vous ne recompilez pas.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a \
sh -c 'apk add --no-cache gcompat 2>&1 | tail -1; /src/glibc/cumul'
OK: 8336 KiB in 19 packages
cumul=33

Le binaire glibc tourne. La ligne OK: d'apk donne l'état total du conteneur (19 paquets, 8336 KiB), pas le poids de gcompat seul : la leçon choisir et maintenir une image de base mesure son ajout à 225 ko dans une couche d'image.

Cette mesure ne porte pourtant que sur un programme trivial, qui n'appelle presque rien de la libc : elle ne garantit pas qu'un programme réel tourne, et un programme réel se vérifie par son test de fumée. Le tableau compare les trois corrections sur ce programme.

CorrectionTourne sous AlpineTourne sous DebianTaille du binaireQuand la choisir
Recompiler dans Alpineouinon18 384 octetscode maîtrisé, livré sur Alpine seulement
Lien statiqueouioui934 520 octetsun seul binaire pour plusieurs socles, hors résolution de noms
gcompatoui, cas trivialoui (binaire inchangé)15 688 octets, plus le paquet gcompatbinaire tiers non recompilable

Pourquoi pip n'installe-t-il pas les mêmes fichiers sur les deux socles ?

Section intitulée « Pourquoi pip n'installe-t-il pas les mêmes fichiers sur les deux socles ? »

Parce que pip, l'installeur de paquets de Python, choisit une roue en fonction de la libc de l'image. Une roue (wheel) est l'archive précompilée d'un paquet Python ; son nom porte une étiquette de plateforme : manylinux pour les systèmes glibc, musllinux pour les systèmes musl. Les mesures de cette section et des suivantes utilisent python:3.14-slim (Debian, glibc 2.41) et python:3.14-alpine (musl), en Python 3.14.10 et pip 26.2.2, sur amd64.

pip debug --verbose liste les étiquettes que l'interpréteur accepte. Regardez le préfixe : c'est lui qui décide.

Fenêtre de terminal
docker run --rm python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966 \
sh -c 'pip debug --verbose 2>/dev/null | grep -E "cp314-cp314-(many|musl)" | head -4'
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'pip debug --verbose 2>/dev/null | grep -E "cp314-cp314-(many|musl)" | head -4'
cp314-cp314-manylinux_2_41_x86_64
cp314-cp314-manylinux_2_40_x86_64
cp314-cp314-manylinux_2_39_x86_64
cp314-cp314-manylinux_2_38_x86_64
cp314-cp314-musllinux_1_2_x86_64
cp314-cp314-musllinux_1_1_x86_64
cp314-cp314-musllinux_1_0_x86_64

Les quatre premières lignes viennent de Debian, les trois dernières d'Alpine. Le socle Debian accepte les roues manylinux, et le socle Alpine n'accepte aucune roue manylinux, seulement musllinux. Pour les étiquettes actuelles, le nombre qui suit le préfixe est la version de libc exigée au minimum (les anciens noms manylinux2010 et manylinux2014 font exception, voir le tableau plus bas) : une roue manylinux_2_28 convient à partir de la glibc 2.28, une roue musllinux_1_2 à partir de musl 1.2. Un même pip install markupsafe télécharge donc deux fichiers différents, ce que le fichier chargé confirme.

Fenêtre de terminal
docker run --rm python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966 \
sh -c 'pip install --no-cache-dir --root-user-action=ignore markupsafe 2>&1 | grep -v notice; python -c "import markupsafe._speedups as s;print(s.__file__)"'
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'pip install --no-cache-dir --root-user-action=ignore markupsafe 2>&1 | grep -v notice; python -c "import markupsafe._speedups as s;print(s.__file__)"'
Downloading markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl (23 kB)
/usr/local/lib/python3.14/site-packages/markupsafe/_speedups.cpython-314-x86_64-linux-gnu.so
Downloading markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl (23 kB)
/usr/local/lib/python3.14/site-packages/markupsafe/_speedups.cpython-314-x86_64-linux-musl.so

Les sorties sont abrégées aux lignes Downloading et à la dernière ligne. L'extension native (un module écrit en C, que Python charge comme une bibliothèque), ici _speedups, porte le suffixe linux-gnu sur Debian et linux-musl sur Alpine. Sur cet exemple, l'installation prend 1,8 s sur Debian et 2,2 s sur Alpine, images déjà tirées : l'idée qu'Alpine oblige à compiler est périmée pour ce paquet.

Les huit paquets testés en ont tous une, au 3 novembre 2026. La commande suivante télécharge la roue de chacun sans l'installer ; --only-binary :all: interdit la compilation, donc un paquet sans roue échouerait.

Fenêtre de terminal
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'cd /tmp; for p in markupsafe psutil orjson pyyaml cryptography pillow grpcio numpy; do printf "%s: " $p; pip download --no-deps --only-binary :all: -d /tmp/d_$p -q $p 2>&1 | tail -1; ls /tmp/d_$p 2>/dev/null; echo; done'

Sous chaque nom de paquet, la commande affiche le fichier de roue téléchargé, précédé d'une ligne [notice] de pip à ignorer. Le tableau reprend le résultat, avec la première étiquette de plateforme du nom de fichier sur Debian (la même commande sur python:3.14-slim) et sur Alpine.

Les noms manylinux2010 et manylinux2014 sont d'anciens noms d'étiquettes, conservés pour les roues déjà publiées : manylinux2010 équivaut à manylinux_2_12 et manylinux2014 à manylinux_2_17, donc à une glibc 2.12 et 2.17 au minimum (PEP 571, PEP 599, et PEP 600 pour la forme manylinux_x_y).

PaquetRoue sur DebianRoue sur Alpine
markupsafemanylinux2014_x86_64musllinux_1_2_x86_64
psutilmanylinux2010_x86_64musllinux_1_2_x86_64
orjsonmanylinux_2_17_x86_64musllinux_1_2_x86_64
pyyamlmanylinux2014_x86_64musllinux_1_2_x86_64
cryptographymanylinux_2_34_x86_64musllinux_1_2_x86_64
pillowmanylinux_2_27_x86_64musllinux_1_2_x86_64
grpciomanylinux2014_x86_64musllinux_1_2_x86_64
numpymanylinux_2_27_x86_64musllinux_1_2_x86_64

Ces huit paquets ne coûtent aucune compilation sur Alpine. Le risque est ailleurs, chez un paquet dont l'éditeur ne publie pas de roue musllinux.

Que se passe-t-il quand un paquet Python n'a pas de roue pour le nouveau socle ?

Section intitulée « Que se passe-t-il quand un paquet Python n'a pas de roue pour le nouveau socle ? »

L'installation échoue sur un message qui ne parle pas de libc, ou pire, elle réussit en livrant un paquet amputé. onnxruntime illustre le premier cas : au 3 novembre 2026, sa roue pour amd64 et Python 3.14 est publiée en manylinux_2_28 seulement.

Fenêtre de terminal
docker run --rm python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966 \
sh -c 'pip install --dry-run --no-deps --only-binary :all: --root-user-action=ignore onnxruntime 2>&1 | grep -v notice | tail -8'
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'pip install --dry-run --no-deps --only-binary :all: --root-user-action=ignore onnxruntime 2>&1 | grep -v notice | tail -8'
Collecting onnxruntime
Downloading onnxruntime-1.31.1-cp314-cp314-manylinux_2_28_x86_64.whl.metadata (5.8 kB)
Would install onnxruntime-1.31.1
ERROR: Could not find a version that satisfies the requirement onnxruntime (from versions: none)
ERROR: No matching distribution found for onnxruntime

Debian trouve la roue ; Alpine répond from versions: none, comme si le paquet n'existait pas. Le message n'évoque ni musl ni roue : sans connaître le mécanisme, vous chercheriez une faute de frappe ou un problème de réseau. Après un changement de FROM, un No matching distribution found sur un paquet qui s'installait avant doit donc vous faire penser à une roue absente pour cette libc. Une version de Python ou une architecture sans roue donne le même message.

Faute de roue, pip peut compiler le paquet à partir de ses sources, ce qui demande un compilateur C. Les deux images Python n'en ont pas (which gcc cc rend le code 1 sur les deux). Forcer la compilation avec --no-binary le montre : psutil échoue sur les deux socles.

Fenêtre de terminal
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'pip install --no-cache-dir --root-user-action=ignore --no-binary psutil psutil 2>&1 | grep -v notice | tail -12'
sudo apk add gcc python3-dev musl-dev linux-headers
error: [Errno 2] No such file or directory: 'gcc'
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for psutil
Failed to build psutil
error: failed-wheel-build-for-install
× Failed to build installable wheels for some pyproject.toml based projects
╰─> psutil

La sortie suggère d'installer gcc et les en-têtes de développement (la commande apk ci-dessus ; sur Debian, sudo apt-get install gcc python3-dev), puis donne l'erreur No such file or directory: 'gcc'. Cette erreur-là est franche. Dans un conteneur, sudo n'existe pas, et sur les images officielles python: mesurées ici, installer le compilateur (build-base sur Alpine, build-essential sur Debian) a suffi à compiler.

Le piège est un paquet qui sait se passer de son extension C : avec --no-binary, markupsafe s'installe sans gcc, parce que l'installation retombe sur une roue py3-none-any, la version en Python pur.

Fenêtre de terminal
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'pip install -q --no-cache-dir --root-user-action=ignore --no-binary markupsafe markupsafe 2>&1 | grep -v notice; python -c "import markupsafe._speedups" 2>&1 | tail -1'
ModuleNotFoundError: No module named 'markupsafe._speedups'

L'installation est verte, la création de la roue markupsafe-3.0.4-py3-none-any.whl aussi, et l'extension _speedups est absente ; le résultat est identique sur Debian. Sans l'option -q, pip affiche la ligne Created wheel for markupsafe: filename=markupsafe-3.0.4-py3-none-any.whl. L'application tourne sans son extension C, sans que rien ne le signale : il faut importer l'extension pour s'en apercevoir.

Avec un compilateur, la compilation produit bien l'extension native, mais elle coûte du temps. Les deux commandes installent le compilateur dans le conteneur puis compilent markupsafe ; la dernière ligne affichée est le chemin de l'extension, cette fois présente.

Fenêtre de terminal
docker run --rm python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2 \
sh -c 'apk add --no-cache build-base >/dev/null 2>&1; pip install -q --no-cache-dir --root-user-action=ignore --no-binary markupsafe markupsafe 2>&1 | grep -v notice; python -c "import markupsafe._speedups as s;print(s.__file__)"'
docker run --rm python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966 \
sh -c 'apt-get update -qq >/dev/null 2>&1; apt-get install -y -qq build-essential >/dev/null 2>&1; pip install -q --no-cache-dir --root-user-action=ignore --no-binary markupsafe markupsafe 2>&1 | grep -v notice; python -c "import markupsafe._speedups as s;print(s.__file__)"'
/usr/local/lib/python3.14/site-packages/markupsafe/_speedups.cpython-314-x86_64-linux-musl.so
/usr/local/lib/python3.14/site-packages/markupsafe/_speedups.cpython-314-x86_64-linux-gnu.so

Chaque commande prend environ 11 s sur Alpine et 24 s sur Debian, contre 2 s pour la roue. Ces durées dépendent du réseau, seul l'ordre de grandeur compte. Deux parades évitent le silence : refuser la compilation implicite avec --only-binary :all:, qui transforme le silence en erreur, et importer dans le Dockerfile ou le test de fumée l'extension native dont l'application dépend.

Quand un paquet indispensable n'a vraiment pas de roue musllinux, il reste deux issues : installer un compilateur et les en-têtes dans l'étape de construction de l'image, ou garder un socle glibc comme python:3.14-slim. La première coûte du temps de construction, la seconde une image plus grosse.

Quand la plateforme change aussi (par exemple de amd64, les processeurs x86 64 bits, à arm64, les processeurs ARM), l'erreur n'est pas forcément exec format error. La leçon choisir et maintenir une image de base montre que exec format error apparaît quand le programme lui-même est d'une autre architecture. Le cas ici est différent : un binaire amd64 glibc lancé dans une image arm64, sur un hôte amd64 sans émulation (aucun outil ne traduit les instructions d'une architecture à l'autre).

Fenêtre de terminal
docker run --rm --platform linux/arm64 -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/glibc/cumul
docker run --rm --platform linux/arm64 -v "$PWD":/src:ro \
debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/glibc/cumul
exec /src/glibc/cumul: no such file or directory
exec /src/glibc/cumul: no such file or directory

Les deux images répondent no such file or directory avec le code 255, pas exec format error. L'explication : le noyau, qui est celui de l'hôte amd64, exécute le binaire x86 sans regarder l'étiquette de l'image ; ce qui manque est son chargeur /lib64/ld-linux-x86-64.so.2, absent de l'image arm64. Le message est donc le même que pour un changement de libc, et il se diagnostique de la même façon : lire l'interpréteur, vérifier qu'il existe.

Sur un hôte arm64, le même binaire amd64 échoue autrement : le noyau ne sait pas exécuter des instructions x86, et la réponse est exec format error, le mécanisme décrit dans choisir et maintenir une image de base. Une première exécution avec --platform tire aussi le contenu arm64 de l'image, qui s'ajoute sur le disque (docker image ls passe de 13 Mo à 26,6 Mo pour Alpine).

Pour vérifier qu'un socle arm64 contient bien le chargeur attendu, docker image inspect --format '{{.Architecture}}' ne sert à rien : il rend amd64 même après un docker run --platform linux/arm64, et seul docker image inspect --platform linux/arm64 rend arm64, avec un autre identifiant d'image. Un ls dans l'image arm64 est lui aussi impossible sans émulation (docker run --platform linux/arm64 … ls rend exec /bin/ls: exec format error, code 255).

La méthode consiste donc à lire l'interpréteur avec file dans l'image de build, puis à lister le contenu de l'image de la plateforme sans l'exécuter : docker create prépare un conteneur qui ne démarre pas, et docker export en écrit le système de fichiers sous forme d'archive que tar -t énumère.

Fenêtre de terminal
c=$(docker create --platform linux/arm64 alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /bin/true)
docker export "$c" | tar -t | grep ld-
docker rm "$c"
lib/ld-musl-aarch64.so.1

La commande docker rm réaffiche l'identifiant du conteneur supprimé, que la sortie ci-dessus omet. Alpine arm64 fournit lib/ld-musl-aarch64.so.1 et aucun ld-linux-x86-64.so.2, donc le binaire amd64 glibc n'y trouve pas son chargeur. Avec Debian, la même commande sur debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 liste usr/lib/ld-linux-aarch64.so.1 et usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1, et là non plus aucun ld-linux-x86-64.so.2.

Pour connaître toutes les plateformes qu'une image publie, interrogez son manifeste.

Fenêtre de terminal
docker buildx imagetools inspect alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a

Pour Alpine, la liste contient amd64, arm/v6, arm/v7, arm64/v8, 386, ppc64le, riscv64 et s390x. Pour changer d'architecture, recompilez pour la plateforme cible ; un binaire déjà construit ne se convertit pas.

Sur un hôte amd64 sans émulation, une image arm64 ne s'exécute pas (voir choisir et maintenir une image de base). Pour la tester quand même, trois voies existent : l'émulation QEMU, que binfmt_misc enregistre auprès du noyau, la compilation croisée (compiler sur l'hôte pour l'architecture cible), ou une machine de l'architecture cible. La machine de mesure n'a pas d'émulation : /proc/sys/fs/binfmt_misc/ n'y contient que python3.13, register et status, sans entrée pour arm64.

Dans quel ordre vérifier avant de changer de FROM ?

Section intitulée « Dans quel ordre vérifier avant de changer de FROM ? »

Dans l'ordre du schéma ci-dessous : le binaire, puis les paquets Python, puis la plateforme, et enfin un test de fumée dans le nouveau socle. Chaque question de gauche correspond à l'une des trois sections précédentes, et chaque « non » ou « oui » qui sort à droite, en rouge, est une correction à faire avant de publier.

Arbre de décision avant un changement de FROM, en trois questions successives : l'interpréteur d'un binaire dynamique existe-t-il dans le nouveau socle (sinon recompiler, lier en statique ou gcompat), une roue existe-t-elle pour la libc du nouveau socle (sinon installer un compilateur ou garder un socle glibc), la plateforme change-t-elle (alors recompiler) ; tous les chemins finissent par un test de fumée de l'application dans le nouveau socle.Vous voulez changerla ligne FROMBinaire dynamique :interpréteur présent ?Paquets Python :roue pour cette libc ?oui ou sans objetRecompiler dans le socle,lier en statique ou gcompatnonPlateformechangée aussi ?oui ou sans objetInstaller un compilateurou garder un socle glibcnonRecompiler pourla nouvelle plateformeouiTest de fumée : lancer l'applicationdans le nouveau soclenon
Les corrections en rouge se font avant de publier ; le test de fumée termine tous les chemins. Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Le test de fumée est l'arbitre. Aucune des lectures précédentes (interpréteur, getconf, étiquettes de roues) n'est une preuve : elles prédisent. Le test lance l'application réelle, ou son binaire, dans l'image candidate, avec ses vraies dépendances, et vérifie le code de sortie et la sortie attendue.

Fenêtre de terminal
docker run --rm -v "$PWD":/src:ro \
alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/musl/cumul
echo "code=$?"
cumul=33
code=0

Pour une application Python, ajoutez à ce test l'importation des extensions natives dont elle dépend, comme dans la section sur les paquets Python. Pour une application qui écoute sur un port, le test est une requête sur ce port. Un changement de FROM ne se valide que par ce lancement.

Comment jouer le test de fumée dans la construction ?

Section intitulée « Comment jouer le test de fumée dans la construction ? »

Le test de fumée se place dans le Dockerfile lui-même : une étape RUN qui lance le binaire ou importe l'extension fait échouer la construction avant toute publication. C'est la dernière case du schéma, jouée à la construction. Premier cas, une construction en plusieurs étapes : la première étape (gcc:14) compile cumul, la seconde copie le binaire dans le socle candidat, passé en argument BASE, et le lance. La valeur par défaut de ARG BASE évite l'avertissement InvalidDefaultArgInFrom, que BuildKit émet quand l'argument d'une ligne FROM n'a pas de valeur.

Enregistrez ce fichier sous le nom Dockerfile, dans ~/changer-socle à côté de cumul.c.

Dockerfile
ARG BASE=debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4
FROM gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bd AS build
COPY cumul.c .
RUN gcc -O2 -o /cumul cumul.c
FROM ${BASE}
COPY --from=build /cumul /usr/local/bin/cumul
RUN /usr/local/bin/cumul

Construisez l'image deux fois, avec Debian puis avec Alpine comme socle candidat. Sur Debian, la construction se termine sans erreur et l'étape RUN affiche cumul=33. Sur Alpine, elle s'arrête à cette étape.

Fenêtre de terminal
docker build --progress=plain --build-arg BASE=debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 -t cumul-test:debian .
docker build --progress=plain --build-arg BASE=alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a -t cumul-test:alpine .
> [stage-1 3/3] RUN /usr/local/bin/cumul:
0.179 /bin/sh: /usr/local/bin/cumul: not found
Dockerfile:8
8 | >>> RUN /usr/local/bin/cumul
ERROR: failed to build: failed to solve: process "/bin/sh -c /usr/local/bin/cumul" did not complete successfully: exit code: 127

Regardez le code de sortie : 127 est celui du shell qui exécute le RUN (/bin/sh -c), qui répond not found parce que l'interpréteur du binaire manque. Le même binaire lancé directement par docker run rend 255, comme vu plus haut : le code dépend de qui lance le programme. L'image n'est pas produite, donc rien d'inutilisable ne part en production.

Deuxième cas, une application Python : l'option --only-binary :all: interdit la compilation, et l'étape suivante importe l'extension native. Enregistrez les deux fichiers suivants dans ~/changer-socle/py.

py/ok.Dockerfile
FROM python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2
RUN pip install --no-cache-dir --only-binary :all: markupsafe
RUN python -c "import markupsafe._speedups"

Ce Dockerfile se construit sans erreur, car markupsafe a une roue musllinux. Le suivant remplace le paquet par onnxruntime, qui n'en a pas.

py/ko.Dockerfile
FROM python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2
RUN pip install --no-cache-dir --only-binary :all: onnxruntime
RUN python -c "import onnxruntime"
Fenêtre de terminal
docker build --progress=plain -f py/ok.Dockerfile -t cumul-test:python-ok py
docker build --progress=plain -f py/ko.Dockerfile -t cumul-test:python-ko py
1.717 ERROR: Could not find a version that satisfies the requirement onnxruntime (from versions: none)
1.719 ERROR: No matching distribution found for onnxruntime
ERROR: failed to build: failed to solve: process "/bin/sh -c pip install --no-cache-dir --only-binary :all: onnxruntime" did not complete successfully: exit code: 1

La construction s'arrête à l'étape pip install avec le code 1 (la sortie est abrégée : les lignes [notice] de pip et le rappel du Dockerfile fautif sont omis). Le message ne parle pas de libc, mais comme il survient à la construction, vous le voyez avant la publication et non au déploiement.

Dans la plupart de ces symptômes, le message accuse un fichier ou un paquet qui existe bel et bien : la cause est ailleurs. Le tableau lie chaque symptôme à sa cause et à la vérification qui la confirme.

SymptômeCause probableVérification
exec /chemin: no such file or directory, code 255, fichier présentinterpréteur ELF absent du nouveau soclefile dans l'image de build, puis ls du chemin lu dans le nouveau socle
ldd d'Alpine répond sans erreur sur un binaire glibcle ldd de musl ne signale pas l'incompatibilitélancer le binaire, ne pas se fier à ldd
No matching distribution found, from versions: noneaucune roue musllinux pour ce paquetpip download --no-deps --only-binary :all: <paquet> dans l'image cible
Failed to build ..., No such file or directory: 'gcc'pip compile, l'image n'a pas de compilateurinstaller gcc et ses en-têtes, ou choisir un paquet avec roue
ModuleNotFoundError: No module named '..._speedups'roue py3-none-any installée par la compilation sans gccimporter l'extension native dans le test de fumée, utiliser --only-binary :all:
Binaire amd64 en image arm64 : no such file or directorychargeur amd64 absent de l'image arm64file dans l'image de build, puis docker create --platform linux/arm64 … et docker export … | tar -t | grep ld- (un ls dans l'image est impossible sans émulation)
docker build s'arrête sur une étape RUN avec not found, code 127interpréteur du binaire absent du socle candidat (le shell du RUN rend 127, docker run rend 255)file dans l'image de build, puis ls du chemin lu dans le socle candidat
Le binaire recompilé pour Alpine échoue sous Debiansymétrie : l'interpréteur musl n'existe pas sur Debianls /lib/ld-musl-* dans l'image

Les mesures laissent des images lourdes, un cache de build et un dossier de travail. L'image gcc:14 pèse 2,14 Go et les deux images Python 178 Mo et 73 Mo (tailles de docker image inspect --format '{{.Size}}') : retirez-les par leur référence, après les images cumul-test construites dans la section précédente. Les images alpine et debian:13-slim servent à d'autres leçons et restent.

Fenêtre de terminal
docker rmi cumul-test:debian cumul-test:python-ok
docker rmi gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bd
docker rmi python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966
docker rmi python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2
cd ~/changer-socle && rm glibc/cumul musl/cumul static/cumul py/ok.Dockerfile py/ko.Dockerfile Dockerfile cumul.c
rmdir glibc musl static py && cd ~ && rmdir ~/changer-socle

Pour vérifier, docker images ne doit plus lister gcc, python ni cumul-test, et ls -d ~/changer-socle doit répondre No such file or directory. Il reste un piège : après des constructions, docker rmi d'une image puis un nouveau docker pull ne la ramènent pas à sa taille d'origine, parce que le cache de build retient son contenu (Alpine reste à 26,6 Mo au lieu de 13 Mo). Il faut d'abord vider ce cache. Cette étape efface tout le cache de build de la machine, pas seulement celui de cette leçon : évitez-la sur une machine partagée. Le cache atteignait 2,6 Go après les mesures.

La séquence suivante vide le cache, retire Alpine, la tire de nouveau par son digest (la variable A le garde) et lui redonne le nom alpine:latest, utile seulement si une autre leçon s'en sert.

Fenêtre de terminal
docker builder prune -af
A=alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a
docker rmi alpine:latest "$A"
docker pull -q "$A" && docker tag "$A" alpine:latest
docker image ls
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
alpine:latest be2656061b53 13MB 3.94MB
alpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a be2656061b53 13MB 3.94MB

Alpine redescend à 13 Mo (mesuré le 3 novembre 2026 avec Docker 29.9.2). La sortie est abrégée aux lignes d'Alpine : docker image ls liste aussi les autres images que vous gardez, comme celle de Debian. Le docker rmi répond No such image pour la seconde référence, que le retrait de la première a déjà emportée, sans conséquence pour la suite.

  1. Changer le FROM change la libc, donc l'interpréteur réclamé par les binaires dynamiques : glibc et musl ne le placent pas au même endroit.

  2. La compatibilité d'un binaire se prévoit en lisant : file dans l'image de build donne l'interpréteur ELF, ls dans le nouveau socle dit s'il existe.

  3. Ne vous fiez pas à ldd dans Alpine : il répond avec le code 0 sur un binaire glibc qui ne peut pas démarrer. getconf GNU_LIBC_VERSION en échec signale une libc qui n'est pas glibc.

  4. Trois corrections pour un binaire glibc sur Alpine : recompiler dans Alpine (le binaire musl échoue alors sous Debian), lier en statique (environ 60 fois plus gros sur ce programme, il tourne sur les deux socles testés, avec une réserve pour la résolution de noms), gcompat (binaire tiers, cas trivial mesuré).

  5. La libc de l'image choisit la roue Python : pip debug --verbose montre manylinux sur Debian et musllinux sur Alpine, et les huit paquets testés ont une roue musllinux.

  6. Le risque est un paquet sans roue musllinux : No matching distribution found ne parle pas de libc. Une installation par compilation sans gcc peut aussi réussir en livrant une roue sans extension C.

  7. Une différence d'architecture ne donne pas toujours exec format error : sur un hôte amd64 sans émulation, un binaire amd64 dans une image arm64 répond no such file or directory ; sur un hôte arm64, ce même binaire répond exec format error.

  8. Le test de fumée tranche : lancer l'application dans le nouveau socle, extensions natives comprises, avant de publier. Placé dans une étape RUN du Dockerfile, il fait échouer la construction : code 127 pour un binaire sans interpréteur, et code 1 dès l'installation pour un paquet sans roue, avant même l'import.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

8 questions
8 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn