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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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
pipchoisit une rouemanylinuxsur Debian etmusllinuxsur 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
Prérequis
Section intitulée « Prérequis »- Un Docker fonctionnel (installation de Docker)
- Avoir suivi la leçon choisir et maintenir une image de base, qui montre pourquoi un binaire glibc échoue sur Alpine
- Savoir lire l'identité d'une image et sa plateforme (la leçon Lire une image OCI et son identité de la même formation)
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.
mkdir -p ~/changer-socle/glibc ~/changer-socle/musl ~/changer-socle/static ~/changer-socle/py && cd ~/changer-socle#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.
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.
docker run --rm -v "$PWD":/src:ro \ debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/glibc/cumuldocker run --rm -v "$PWD":/src:ro \ alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/glibc/cumulSur 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.
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.2lrwxrwxrwx 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.2Alpine 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.
Pourquoi ne pas se fier à ldd dans Alpine ?
Section intitulée « Pourquoi ne pas se fier à ldd dans Alpine ? »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.
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.
Comment identifier la libc d'un socle ?
Section intitulée « Comment identifier la libc d'un socle ? »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.
docker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a getconf GNU_LIBC_VERSIONdocker run --rm alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a ldd --versiondocker run --rm debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 getconf GNU_LIBC_VERSIONgetconf: GNU_LIBC_VERSION: unknown variablemusl libc (x86_64)Version 1.2.6Dynamic Program LoaderUsage: /lib/ld-musl-x86_64.so.1 [options] [--] pathnameglibc 2.41getconf 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.
Recompiler contre musl dans Alpine
Section intitulée « Recompiler contre musl dans Alpine »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.
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.triggerOK: 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.
docker run --rm -v "$PWD":/src:ro \ alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/musl/cumuldocker run --rm -v "$PWD":/src:ro \ debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/musl/cumulSous 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.
Lier le binaire en statique
Section intitulée « Lier le binaire en statique »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.
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 executableldd répond not a dynamic executable avec le code 1 : ici ce code est attendu. Lancez le binaire dans les deux socles.
docker run --rm -v "$PWD":/src:ro \ alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/static/cumuldocker run --rm -v "$PWD":/src:ro \ debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/static/cumulCe 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.
Ajouter gcompat quand on ne peut pas recompiler
Section intitulée « Ajouter gcompat quand on ne peut pas recompiler »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.
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 packagescumul=33Le 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.
| Correction | Tourne sous Alpine | Tourne sous Debian | Taille du binaire | Quand la choisir |
|---|---|---|---|---|
| Recompiler dans Alpine | oui | non | 18 384 octets | code maîtrisé, livré sur Alpine seulement |
| Lien statique | oui | oui | 934 520 octets | un seul binaire pour plusieurs socles, hors résolution de noms |
gcompat | oui, cas trivial | oui (binaire inchangé) | 15 688 octets, plus le paquet gcompat | binaire 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.
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_64Les 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.
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.soDownloading 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.soLes 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.
Quels paquets courants ont une roue musllinux ?
Section intitulée « Quels paquets courants ont une roue musllinux ? »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.
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).
| Paquet | Roue sur Debian | Roue sur Alpine |
|---|---|---|
markupsafe | manylinux2014_x86_64 | musllinux_1_2_x86_64 |
psutil | manylinux2010_x86_64 | musllinux_1_2_x86_64 |
orjson | manylinux_2_17_x86_64 | musllinux_1_2_x86_64 |
pyyaml | manylinux2014_x86_64 | musllinux_1_2_x86_64 |
cryptography | manylinux_2_34_x86_64 | musllinux_1_2_x86_64 |
pillow | manylinux_2_27_x86_64 | musllinux_1_2_x86_64 |
grpcio | manylinux2014_x86_64 | musllinux_1_2_x86_64 |
numpy | manylinux_2_27_x86_64 | musllinux_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.
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 onnxruntimeDebian 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.
Que se passe-t-il si pip compile à la place ?
Section intitulée « Que se passe-t-il si pip compile à la place ? »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.
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 psutilFailed to build psutil
error: failed-wheel-build-for-install
× Failed to build installable wheels for some pyproject.toml based projects╰─> psutilLa 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.
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.
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.soChaque 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.
Et si l'architecture change avec le socle ?
Section intitulée « Et si l'architecture change avec le socle ? »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).
docker run --rm --platform linux/arm64 -v "$PWD":/src:ro \ alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/glibc/cumuldocker run --rm --platform linux/arm64 -v "$PWD":/src:ro \ debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4 /src/glibc/cumulexec /src/glibc/cumul: no such file or directoryexec /src/glibc/cumul: no such file or directoryLes 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.
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.1La 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.
docker buildx imagetools inspect alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9aPour 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.
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.
docker run --rm -v "$PWD":/src:ro \ alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a /src/musl/cumulecho "code=$?"cumul=33code=0Pour 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.
ARG BASE=debian:13-slim@sha256:f6b08b09a05725f9431502d3b5714f5c4576e82c1a84fa1debe0c24a7e0c3ec4FROM gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bd AS buildCOPY cumul.c .RUN gcc -O2 -o /cumul cumul.c
FROM ${BASE}COPY --from=build /cumul /usr/local/bin/cumulRUN /usr/local/bin/cumulConstruisez 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.
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 foundDockerfile:8 8 | >>> RUN /usr/local/bin/cumulERROR: failed to build: failed to solve: process "/bin/sh -c /usr/local/bin/cumul" did not complete successfully: exit code: 127Regardez 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.
FROM python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2RUN pip install --no-cache-dir --only-binary :all: markupsafeRUN 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.
FROM python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2RUN pip install --no-cache-dir --only-binary :all: onnxruntimeRUN python -c "import onnxruntime"docker build --progress=plain -f py/ok.Dockerfile -t cumul-test:python-ok pydocker build --progress=plain -f py/ko.Dockerfile -t cumul-test:python-ko py1.717 ERROR: Could not find a version that satisfies the requirement onnxruntime (from versions: none)1.719 ERROR: No matching distribution found for onnxruntimeERROR: 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: 1La 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.
Dépannage
Section intitulée « Dépannage »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ôme | Cause probable | Vérification |
|---|---|---|
exec /chemin: no such file or directory, code 255, fichier présent | interpréteur ELF absent du nouveau socle | file dans l'image de build, puis ls du chemin lu dans le nouveau socle |
ldd d'Alpine répond sans erreur sur un binaire glibc | le ldd de musl ne signale pas l'incompatibilité | lancer le binaire, ne pas se fier à ldd |
No matching distribution found, from versions: none | aucune roue musllinux pour ce paquet | pip 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 compilateur | installer 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 gcc | importer l'extension native dans le test de fumée, utiliser --only-binary :all: |
Binaire amd64 en image arm64 : no such file or directory | chargeur amd64 absent de l'image arm64 | file 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 127 | interpré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 Debian | symétrie : l'interpréteur musl n'existe pas sur Debian | ls /lib/ld-musl-* dans l'image |
Comment nettoyer après les mesures ?
Section intitulée « Comment nettoyer après les mesures ? »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.
docker rmi cumul-test:debian cumul-test:python-okdocker rmi gcc:14@sha256:435c3cbc2ad1c25fbc1684c367808ff0ca2035a1bed56734a40165ab5cd7e3bddocker rmi python:3.14-slim@sha256:930e1abba7c4e8f96ed93e8012c35d174d7732752731bbd5e8e757d7ec6f3966docker rmi python:3.14-alpine@sha256:eb2c62658b8d51bff0acc408504b76521d02030de56a8a3c1e830083e89b89d2cd ~/changer-socle && rm glibc/cumul musl/cumul static/cumul py/ok.Dockerfile py/ko.Dockerfile Dockerfile cumul.crmdir glibc musl static py && cd ~ && rmdir ~/changer-soclePour 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.
docker builder prune -afA=alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9adocker rmi alpine:latest "$A"docker pull -q "$A" && docker tag "$A" alpine:latestdocker image lsIMAGE ID DISK USAGE CONTENT SIZE EXTRAalpine:latest be2656061b53 13MB 3.94MBalpine@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a be2656061b53 13MB 3.94MBAlpine 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.
À retenir
Section intitulée « À retenir »-
Changer le
FROMchange la libc, donc l'interpréteur réclamé par les binaires dynamiques : glibc et musl ne le placent pas au même endroit. -
La compatibilité d'un binaire se prévoit en lisant :
filedans l'image de build donne l'interpréteur ELF,lsdans le nouveau socle dit s'il existe. -
Ne vous fiez pas à
ldddans Alpine : il répond avec le code 0 sur un binaire glibc qui ne peut pas démarrer.getconf GNU_LIBC_VERSIONen échec signale une libc qui n'est pas glibc. -
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é). -
La libc de l'image choisit la roue Python :
pip debug --verbosemontremanylinuxsur Debian etmusllinuxsur Alpine, et les huit paquets testés ont une rouemusllinux. -
Le risque est un paquet sans roue
musllinux:No matching distribution foundne parle pas de libc. Une installation par compilation sansgccpeut aussi réussir en livrant une roue sans extension C. -
Une différence d'architecture ne donne pas toujours
exec format error: sur un hôteamd64sans émulation, un binaireamd64dans une imagearm64répondno such file or directory; sur un hôtearm64, ce même binaire répondexec format error. -
Le test de fumée tranche : lancer l'application dans le nouveau socle, extensions natives comprises, avant de publier. Placé dans une étape
RUNdu 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.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Séparer construction et exécution
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications