Choisir son image de base
Pourquoi
La première ligne d'un Dockerfile, FROM, est la décision la plus lourde de conséquences de toute l'image, et c'est souvent celle qu'on prend le moins consciemment : on recopie celle du tutoriel. Pourtant elle détermine à elle seule :
- l'essentiel de la taille de l'image, donc la durée des téléchargements à chaque déploiement et le coût de stockage dans les registres ;
- l'essentiel de la surface d'attaque : chaque paquet présent est du code qu'un attaquant peut utiliser, et une vulnérabilité à suivre ;
- la compatibilité de l'application : bibliothèque C, versions des bibliothèques système, disponibilité des paquets binaires de votre langage ;
- le rythme et la qualité des correctifs : qui publie les mises à jour de sécurité, à quelle vitesse, pendant combien d'années.
Dans le cours Docker : les fondamentaux, nous avons construit l'image de Signalements sur python:3.14-slim sans discuter ce choix. L'analyse Trivy de la leçon 12 y avait trouvé 51 vulnérabilités hautes dans les paquets Debian, presque toutes dans des paquets dont l'application ne se sert jamais. Cette leçon reprend la question à la base : quelles images existent, comment les comparer avec des mesures plutôt qu'avec des réputations, et comment choisir.
Les concepts
Ce que contient une image de base
Une image de base fournit tout ce que votre application ne fournit pas elle-même :
- une bibliothèque C (glibc pour Debian, Ubuntu et Red Hat ; musl pour Alpine), indispensable à presque tout programme qui n'est pas lié statiquement ;
- des bibliothèques système : OpenSSL pour le TLS, zlib, les certificats des autorités de confiance, les données de fuseaux horaires ;
- éventuellement un environnement d'exécution (l'interpréteur Python, une JVM, Node.js) ;
- éventuellement des outils : un shell,
aptouapk,coreutils,mount,login... utiles pour construire et déboguer, inutiles et risqués à l'exécution.
Le noyau, lui, n'en fait jamais partie (cours précédent, leçon 1).
Les grandes familles
| Famille | Exemples | Ce qu'on y trouve |
|---|---|---|
| Distribution complète | debian:13, ubuntu:24.04 | Le système minimal de la distribution, avec son gestionnaire de paquets |
| Variante « slim » | debian:13-slim, python:3.14-slim | La même chose, débarrassée de la documentation, des pages de manuel et des traductions |
| Distribution minimaliste | alpine:3.22 | BusyBox, musl et apk : quelques mégaoctets |
| Distribution d'éditeur | registry.access.redhat.com/ubi9/ubi-minimal | Red Hat Enterprise Linux redistribuable (Universal Base Image), pour les environnements certifiés Red Hat |
| Distroless | gcr.io/distroless/static-debian13, gcr.io/distroless/python3-debian13 | Seulement les fichiers d'exécution, tirés de Debian : ni shell, ni gestionnaire de paquets |
| Images « durcies » | cgr.dev/chainguard/python, Docker Hardened Images (dhi.io) | Images minimales reconstruites en continu, avec une promesse de correctifs rapides |
| Vide | scratch | Rien du tout : seulement ce que vous y copiez |
Les images officielles des langages (python, node, golang, eclipse-temurin) sont construites par-dessus une distribution : python:3.14-slim part de debian:trixie-slim, python:3.14-alpine part d'Alpine. Leur étiquette indique la variante.
Les critères de choix
- Taille. Elle se mesure de deux façons : la taille compressée, celle qui transite sur le réseau et occupe le registre, et la taille décompressée sur le disque.
- Surface d'attaque. Le nombre de paquets, et surtout la présence d'outils exploitables par un attaquant qui aurait pris pied dans le conteneur : shell, gestionnaire de paquets, client réseau.
- Vulnérabilités connues, et surtout corrigeables : le chiffre brut dépend énormément de la politique de la distribution (nous allons le voir).
- Compatibilité : glibc ou musl, versions disponibles de votre langage, roues binaires (wheels) Python, modules natifs Node.js.
- Rythme de mise à jour : à quelle fréquence l'image est reconstruite, à quelle vitesse les correctifs de sécurité y arrivent.
- Support et durée de vie : combien d'années la version recevra des correctifs, qui les garantit, et sous quelles conditions (gratuit, abonnement).
- Débogabilité : pouvoir ouvrir un shell dans un conteneur en production est confortable ; une image sans shell demande d'autres méthodes (cours précédent, leçon 6).
Il n'existe pas d'image qui gagne sur tous les critères. Le reste de la leçon mesure ces compromis.
glibc et musl
La bibliothèque C est la couche entre les programmes et le noyau : elle fournit malloc, printf, la résolution DNS, la gestion des locales, le chargement des bibliothèques dynamiques. Debian, Ubuntu et Red Hat utilisent glibc, la bibliothèque GNU ; Alpine utilise musl, plus petite et plus simple. Les deux implémentent les mêmes normes, mais pas exactement de la même façon, et surtout elles ne sont pas interchangeables au niveau binaire : un programme compilé et lié dynamiquement contre glibc ne peut pas s'exécuter sur un système musl, et inversement. Le wiki du projet musl tient une liste détaillée des différences de comportement (résolution DNS, locales, taille de pile des threads par défaut, allocateur mémoire).
En pratique
Note
Les mesures de cette leçon ont été prises le 1er octobre 2026. Les tailles et surtout les nombres de vulnérabilités évoluent chaque semaine : refaites-les avant de décider, avec les mêmes commandes.
Mesurer la taille
Avec le magasin d'images containerd, par défaut depuis Docker 29, docker image ls --tree affiche pour chaque plateforme l'occupation sur le disque et la taille du contenu compressé :
$ docker image ls --tree debian:13-slim
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
debian:13-slim ...
├─ linux/amd64 7792b1f7702a 117MB 29.8MB
...
Voici les mesures pour les images de base les plus courantes, sur linux/amd64 :
| Image | Système | Sur le disque | Compressée |
|---|---|---|---|
debian:13 | Debian 13.7 | 184 Mo | 49,4 Mo |
debian:13-slim | Debian 13.7 | 117 Mo | 29,8 Mo |
ubuntu:24.04 | Ubuntu 24.04 | 117 Mo | 29,8 Mo |
registry.access.redhat.com/ubi9/ubi-minimal | RHEL 9.8 | 153 Mo | 40,7 Mo |
alpine:3.22 | Alpine 3.22.6 | 12,8 Mo | 3,79 Mo |
gcr.io/distroless/static-debian13:nonroot | Debian 13.7 | 6,64 Mo | 862 ko |
cgr.dev/chainguard/static | Wolfi | 6,37 Mo | 610 ko |
L'égalité parfaite entre debian:13-slim et ubuntu:24.04 est une coïncidence d'arrondi ; le registre donne la taille exacte des couches :
$ for i in debian:13-slim ubuntu:24.04; do
d=$(docker buildx imagetools inspect $i --raw | jq -r '.manifests[] | select(.platform.architecture=="amd64" and .platform.os=="linux") | .digest')
printf "%s " $i; docker buildx imagetools inspect "$i@$d" --raw | jq '[.layers[].size] | add'
done
debian:13-slim 29830418
ubuntu:24.04 29764116
La première commande lit l'index de l'image et y prend l'empreinte du manifeste linux/amd64 ; la seconde lit ce manifeste et additionne la taille de ses couches (leçon 7 du cours précédent).
Ces 29 830 418 octets sont exactement la taille de la première couche de python:3.14-slim (vérifiez-le avec la même commande sur cette image, ou avec le script de la leçon 7 du cours précédent). L'image Python slim est debian:trixie-slim plus quelques couches. Les couches partagées ne sont stockées et téléchargées qu'une fois : si toutes vos images partent de la même base, elle ne coûte qu'une fois par serveur.
Pour un environnement d'exécution, l'écart entre variantes est spectaculaire :
| Image | Sur le disque | Compressée |
|---|---|---|
python:3.14 | 1,61 Go | 415 Mo |
python:3.14-slim | 178 Mo | 43,5 Mo |
python:3.14-alpine | 73 Mo | 17,8 Mo |
gcr.io/distroless/python3-debian13:nonroot | 90,5 Mo | 22,6 Mo |
cgr.dev/chainguard/python | 111 Mo | 29,8 Mo |
L'image python:3.14 complète part de buildpack-deps, qui embarque compilateurs, en-têtes de développement et outils de gestion de sources : elle est faite pour construire, pas pour exécuter. La leçon suivante montre comment s'en servir pour l'un sans la garder pour l'autre.
Mesurer la surface d'attaque et les vulnérabilités
Trivy (cours précédent, leçon 12) compte les paquets et les vulnérabilités. Pour ne pas monter le socket Docker, on lui donne l'image sous forme d'archive :
$ docker save debian:13-slim -o img.tar
$ docker run --rm -v "$PWD":/t:ro -v trivy-cache:/root/.cache aquasec/trivy:0.74.0 \
image --quiet --input /t/img.tar --scanners vuln --list-all-pkgs --format json \
| jq '[.Results[]?.Packages? // [] | length] | add'
78
En répétant la mesure sur chaque image, puis en comptant les vulnérabilités hautes et critiques, avec et sans l'option --ignore-unfixed qui ne garde que celles pour lesquelles un correctif existe :
| Image | Paquets | Vulnérabilités (toutes sévérités) | Hautes et critiques | dont corrigeables |
|---|---|---|---|---|
debian:13-slim | 78 | 188 | 48 | 5 |
ubuntu:24.04 | 92 | 21 | 1 | 1 |
ubi9/ubi-minimal | 109 | 205 | 13 | 0 |
alpine:3.22 | 16 | 0 | 0 | 0 |
distroless/static-debian13 | 6 | 0 | 0 | 0 |
chainguard/static | 3 | 0 | 0 | 0 |
python:3.14 | 488 | 4 122 | 371 | non mesuré |
python:3.14-slim | 106 | 209 | 55 | 11 |
python:3.14-alpine | 49 | 6 | 4 | 4 |
distroless/python3-debian13 | 38 | 167 | 24 | 2 |
chainguard/python | 31 | 0 | 0 | 0 |
Trois enseignements, qui valent bien plus que le classement :
1. Le nombre brut dépend de la politique de la distribution. Debian 13 affiche 48 vulnérabilités hautes, Ubuntu 24.04 une seule, avec pourtant des paquets très proches. Debian publie dans son traqueur de sécurité toutes les vulnérabilités connues de ses paquets, y compris celles qu'elle juge mineures et ne corrigera pas dans une version stable (statut no-dsa ou ignored). Ubuntu et Red Hat trient différemment. Trivy reprend les données de chaque distribution. Comparer deux distributions par leur nombre brut de vulnérabilités, c'est comparer deux politiques de publication, pas deux niveaux de sécurité.
2. Le chiffre utile est celui des vulnérabilités corrigeables. Sur les 48 de debian:13-slim, 5 ont un correctif disponible. C'est la liste d'action : un correctif existe, l'image n'est pas à jour. Les autres demandent une analyse d'exposition (le composant est-il utilisé ?), pas une reconstruction.
3. Les vulnérabilités viennent aussi de là où on ne les attend pas. Détaillons les vulnérabilités corrigeables des deux images Python :
$ docker save python:3.14-slim -o img.tar
$ docker run --rm -v "$PWD":/t:ro -v trivy-cache:/root/.cache aquasec/trivy:0.74.0 \
image --quiet --input /t/img.tar --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json \
| jq -r '.Results[] | select(.Vulnerabilities) | .Type as $ty | .Vulnerabilities[] | "\($ty) \(.PkgName) \(.InstalledVersion) -> \(.FixedVersion)"' \
| sort | uniq -c
1 debian libpcre2-8-0 10.46-1~deb13u2 -> 10.46-1~deb13u3
2 debian libssl3t64 3.5.7-1~deb13u2 -> 3.5.7-1~deb13u3
2 debian openssl 3.5.7-1~deb13u2 -> 3.5.7-1~deb13u3
2 debian openssl-provider-legacy 3.5.7-1~deb13u2 -> 3.5.7-1~deb13u3
1 python-pkg msgpack 1.1.2 -> 1.2.1
1 python-pkg setuptools 70.3.0 -> 78.1.1
2 python-pkg urllib3 2.7.0 -> 2.8.0
Pour python:3.14-alpine, la liste se réduit aux trois dernières lignes (quatre vulnérabilités), identiques. msgpack, setuptools et urllib3 ne sont pas des dépendances de Signalements : ce sont des bibliothèques embarquées dans pip lui-même (son répertoire pip/_vendor). pip ne sert qu'à installer les dépendances pendant la construction ; il n'a rien à faire dans l'image d'exécution. Les constructions multi-étapes de la leçon suivante le font disparaître, avec ses vulnérabilités.
Quant à OpenSSL : l'image python:3.14-slim a été construite le 19 septembre, et Debian a publié depuis une version corrigée (deb13u3). Une image de base est une photographie ; elle vieillit dès sa publication.
Le rythme de mise à jour
$ for i in debian:13-slim ubuntu:24.04 alpine:3.22 python:3.14-slim python:3.14-alpine \
gcr.io/distroless/python3-debian13:nonroot cgr.dev/chainguard/python:latest \
registry.access.redhat.com/ubi9/ubi-minimal:latest; do
printf "%-48s %s\n" $i "$(docker image inspect $i --format '{{.Created}}' | cut -c1-10)"
done
debian:13-slim 2026-09-18
ubuntu:24.04 2026-09-11
alpine:3.22 2026-09-17
python:3.14-slim 2026-09-19
python:3.14-alpine 2026-09-17
gcr.io/distroless/python3-debian13:nonroot 1970-01-01
cgr.dev/chainguard/python:latest 2026-09-29
registry.access.redhat.com/ubi9/ubi-minimal:latest 2026-09-30
Les images officielles Docker sont reconstruites régulièrement (à chaque nouvelle version de la distribution ou du langage, et pour les correctifs importants) ; Chainguard et Red Hat reconstruisent en continu. Et l'image distroless affiche... 1970 : elle est construite avec Bazel de façon reproductible, et ses horodatages sont fixés à zéro, l'époque Unix. Sa date ne dit donc rien de sa fraîcheur ; seule l'empreinte change. Nous reviendrons sur la reproductibilité à la leçon 6.
Construire Signalements sur trois bases
Le Dockerfile de Signalements ne change que par sa ligne FROM (et, pour Alpine, par la commande de création d'utilisateur : BusyBox fournit adduser, pas useradd) :
RUN adduser -S -u 10001 -H -s /sbin/nologin appli$ for v in 3.14 3.14-slim 3.14-alpine; do
/usr/bin/time -f "$v construction %e s" docker build -q --no-cache -t signalements:$v -f Dockerfile.$v . >/dev/null
done
3.14 construction 8.33 s
3.14-slim construction 8.05 s
3.14-alpine construction 7.18 s
$ for v in 3.14 3.14-slim 3.14-alpine; do
docker image ls --tree signalements:$v | grep linux/amd64 | awk -v n=$v '{print n, "disque="$4, "compresse="$5}'
done
3.14 disque=1.65GB compresse=423MB
3.14-slim disque=217MB compresse=51.8MB
3.14-alpine disque=108MB compresse=25.2MB
La construction sur Alpine fonctionne et l'image fait la moitié de la variante slim. Ce n'était pas acquis : il y a encore quelques années, pip install psycopg[binary] sur Alpine échouait ou compilait pendant de longues minutes, faute de roues binaires pour musl. Depuis la norme musllinux (PEP 656, 2021), les projets populaires en publient :
$ docker run --rm --entrypoint sh signalements:3.14-alpine -c 'ls /usr/local/lib/python3.14/site-packages | grep -i psycopg_binary; ldd --version 2>&1 | head -1'
psycopg_binary
psycopg_binary-3.3.6.dist-info
psycopg_binary.libs
musl libc (x86_64)
Le jour où une de vos dépendances n'a pas de roue musllinux, pip la compilera depuis les sources : il faudra alors installer un compilateur et des en-têtes dans l'image, et la construction prendra beaucoup plus longtemps.
Et les performances ?
La réputation d'Alpine veut que Python y soit « beaucoup plus lent ». Mesurons sur un petit banc d'essai (sérialisation JSON, expressions régulières, allocations massives), limité à un cœur avec --cpus 1 :
import json, re, time
def mesurer(nom, fonction, repetitions=5):
durees = []
for _ in range(repetitions):
debut = time.perf_counter()
fonction()
durees.append(time.perf_counter() - debut)
print(f"{nom:<28} {min(durees)*1000:8.1f} ms")
donnees = [{"id": i, "lieu": f"Rue {i}", "description": "Lampadaire éteint " * 5} for i in range(50_000)]
texte = json.dumps(donnees)
motif = re.compile(r"Rue (\d+)")
mesurer("json.dumps (50 000 objets)", lambda: json.dumps(donnees))
mesurer("json.loads", lambda: json.loads(texte))
mesurer("regex sur 2,3 Mo", lambda: motif.findall(texte))
mesurer("allocations (2 M petits objets)", lambda: [{"a": i} for i in range(2_000_000)])Trois séries, sur une machine de développement qui faisait aussi tourner d'autres conteneurs :
| Mesure (meilleur de 5) | slim, série 1 | alpine, série 1 | slim, série 2 | alpine, série 2 | slim, série 3 | alpine, série 3 |
|---|---|---|---|---|---|---|
json.dumps | 17,6 ms | 19,6 ms | 17,2 ms | 19,0 ms | 17,8 ms | 19,1 ms |
json.loads | 44,8 ms | 37,0 ms | 42,5 ms | 38,4 ms | 44,0 ms | 37,6 ms |
| expression régulière | 4,1 ms | 13,3 ms | 4,4 ms | 6,1 ms | 4,5 ms | 13,3 ms |
| allocations | 763 ms | 1 094 ms | 851 ms | 971 ms | 1 063 ms | 863 ms |
Le verdict est nuancé : json.dumps est un peu plus lent sur musl, json.loads un peu plus rapide, les allocations varient autant d'une série à l'autre que d'une image à l'autre. Seule l'expression régulière est toujours plus lente sur Alpine, et nettement dans deux séries sur trois. Pour une API comme Signalements, qui passe son temps à attendre la base de données, la différence sera invisible ; pour un traitement de calcul intensif, elle peut compter. La seule réponse fiable est de mesurer votre charge de travail, pas de se fier à une réputation (dans un sens ou dans l'autre).
Le piège musl, en direct
Le vrai risque d'Alpine n'est pas la performance, c'est la compatibilité binaire. Compilons signalements-export, l'outil Go du cours, avec les réglages par défaut de l'image golang:1.26 (basée sur Debian, donc glibc), puis lançons-le dans Alpine :
$ docker run --rm -v "$PWD/export":/src -w /src -e GOFLAGS=-buildvcs=false golang:1.26 \
sh -c 'go build -o se-glibc . && CGO_ENABLED=0 go build -o se-static .'
$ file export/se-glibc export/se-static
export/se-glibc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, ...
export/se-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, ...
$ docker run --rm -v "$PWD/export":/x:ro alpine:3.22 /x/se-glibc -version
exec /x/se-glibc: no such file or directory
$ docker run --rm -v "$PWD/export":/x:ro alpine:3.22 /x/se-static -version
signalements-export dev (linux/amd64, go1.26.8)
no such file or directory... alors que le fichier existe bel et bien :
$ docker run --rm -v "$PWD/export":/x:ro alpine:3.22 sh -c 'ls -l /x/se-glibc; ls /lib64/ld-linux-x86-64.so.2'
-rwxr-xr-x 1 1000 1000 8995256 Oct 1 11:41 /x/se-glibc
ls: /lib64/ld-linux-x86-64.so.2: No such file or directory
Le fichier introuvable n'est pas le programme, mais son chargeur dynamique (interpreter), /lib64/ld-linux-x86-64.so.2, celui de glibc, qu'Alpine n'a pas. Le noyau renvoie l'erreur ENOENT pour le chargeur, et le message laisse croire que c'est le programme qui manque. C'est l'une des erreurs les plus déroutantes du monde des conteneurs ; vous la reconnaîtrez désormais. La version compilée avec CGO_ENABLED=0 est statique : elle n'a besoin d'aucune bibliothèque C et tourne sur n'importe quel Linux de la même architecture, y compris sur scratch, moyennant quelques fichiers que la leçon 5 détaille.
Sous le capot
Pourquoi une image slim garde le même nombre de paquets. debian:13 et debian:13-slim comptent tous deux 78 paquets. La différence de 67 Mo vient de fichiers que la variante slim supprime à l'intérieur des paquets installés : documentation, pages de manuel, traductions (/usr/share/doc, /usr/share/man, /usr/share/locale), grâce à des règles d'exclusion de dpkg (path-exclude dans /etc/dpkg/dpkg.cfg.d/). Ces règles s'appliquent aussi aux paquets que vous installez ensuite.
D'où viennent les vulnérabilités que trouve Trivy. Trivy identifie chaque paquet (base de données dpkg ou apk, métadonnées des bibliothèques Python, Go, Java...) puis consulte les avis de sécurité de la source : traqueur de sécurité Debian, Ubuntu CVE Tracker, base de sécurité d'Alpine, données de Red Hat, base GitHub pour les bibliothèques des langages. Si une distribution ne publie pas une vulnérabilité pour un paquet, Trivy ne la voit pas ; si elle la publie avec le statut « ne sera pas corrigée », Trivy l'affiche sans version corrigée.
Comment Alpine et Wolfi tiennent dans si peu. Alpine remplace les outils GNU par BusyBox, un unique binaire qui implémente des centaines de commandes, et utilise musl. Wolfi, la distribution de Chainguard, garde glibc (donc la compatibilité binaire avec Debian), mais découpe tout en paquets très fins et ne construit que ce qui est nécessaire. Distroless prend les paquets Debian et n'en extrait que les fichiers d'exécution, sans les métadonnées de dpkg nécessaires pour en installer d'autres.
Pourquoi l'image distroless Python est en 3.13. Elle ne compile pas Python : elle reprend le paquet Python de Debian 13, qui est en version 3.13 :
$ docker run --rm gcr.io/distroless/python3-debian13:nonroot --version
Python 3.13.5
Vous ne choisissez donc pas votre version de Python sur distroless : vous prenez celle de la distribution. Pour un projet qui exige Python 3.14, c'est rédhibitoire ; pour un projet qui accepte la version de Debian, c'est un gage de stabilité et de correctifs suivis par l'équipe de sécurité Debian.
Pièges courants
exec ...: no such file or directory pour un fichier qui existe. Le chargeur dynamique est absent : binaire glibc sur Alpine, binaire musl sur Debian, ou binaire d'une autre architecture. file ou readelf -l donnent l'interpréteur attendu.
Choisir Alpine pour la taille, puis y installer un compilateur. Une dépendance sans roue musllinux, et l'image Alpine se retrouve avec gcc, musl-dev et des en-têtes, plus grosse et plus lente à construire que la version slim. Vérifiez la disponibilité des roues de vos dépendances avant de choisir, ou séparez construction et exécution (leçon 2).
Comparer des nombres bruts de vulnérabilités entre distributions. Debian publie davantage que les autres ; un rapport de 48 vulnérabilités sur Debian n'est pas « pire » que 1 sur Ubuntu. Comparez les vulnérabilités corrigeables, et, à distribution égale, le nombre de paquets.
Les différences de comportement de musl. La plus connue concerne le DNS : le résolveur de musl n'a longtemps pas su basculer en TCP pour les réponses volumineuses (corrigé dans musl 1.2.4 ; Alpine 3.22 embarque la 1.2.5 et python:3.14-alpine, construite sur Alpine 3.24, la 1.2.6) et il interroge tous les serveurs DNS en parallèle au lieu de les essayer dans l'ordre. D'autres différences touchent les locales et la taille par défaut de la pile des threads, ce qui peut faire planter un programme très récursif. Le wiki de musl les recense : lisez-le avant de déployer sur Alpine une application qui n'y a jamais tourné.
Les images gratuites aux conditions mouvantes. Les images gratuites de Chainguard n'existent qu'en étiquette latest (et latest-dev) : impossible d'épingler une version mineure de Python sans abonnement, et elles ne sont pas couvertes par l'engagement de délai de correction. Les Docker Hardened Images sont gratuites et libres depuis décembre 2025, mais se téléchargent depuis dhi.io après authentification. Vérifiez les conditions au moment de choisir, et prévoyez une solution de repli.
Utiliser python:3.14 (sans variante) en production. 1,6 Go, près de 500 paquets et des milliers de vulnérabilités, pour faire tourner un interpréteur. Cette image est faite pour construire.
Sécurité
- Moins de paquets, moins de risques. Chaque outil présent dans l'image (shell,
apt,curl,mount) est un outil offert à un attaquant qui exploite une faille de l'application, et une source de vulnérabilités à trier. C'est l'argument principal des images minimales, avant la taille. - L'origine compte. Une image de base est du logiciel tiers exécuté en production. Préférez les images officielles, celles d'éditeurs vérifiés, ou des images que vous reconstruisez vous-même ; épinglez-les par empreinte (leçon 6) et vérifiez leurs signatures quand l'éditeur en publie (leçon 9).
- Les mises à jour comptent plus que le point de départ. Une image minimale non mise à jour finit par être plus vulnérable qu'une image complète reconstruite chaque semaine. Le critère « rythme de correctifs » pèse autant que le critère « taille ».
- Le cadre réglementaire. Pour les clients soumis à NIS2 ou aux exigences de l'ANSSI, la question « d'où vient ce composant et qui le maintient ? » devient une obligation de documentation. Une politique d'images de base écrite (liste blanche, fréquence de reconstruction, seuil de vulnérabilités corrigeables accepté) y répond.
En production
- Standardisez. Une organisation qui laisse chaque équipe choisir sa base se retrouve avec vingt bases différentes, autant de mises à jour à suivre et aucun partage de couches. Définissez deux ou trois images de base approuvées (par exemple une base Debian slim pour les applications, une base distroless ou
scratchpour les binaires statiques), éventuellement construites et republiées dans votre propre registre (rg.fr-par.scw.cloudchez Lyneko), avec vos certificats d'autorité internes et votre configuration de fuseau horaire. - Reconstruisez régulièrement, même sans changement de code : une reconstruction hebdomadaire avec
docker build --pullrécupère les correctifs de la base. La leçon 10 l'intègre à la chaîne CI. - Automatisez le suivi. Des outils comme Renovate détectent les nouvelles versions de vos images de base et ouvrent des demandes de mise à jour (leçon 6).
- Gardez une voie de débogage. Si vous choisissez une base sans shell, préparez la méthode de diagnostic à l'avance : conteneur d'outillage partageant les namespaces (cours précédent, leçon 6), ou variante
-dev/debugde l'image utilisée seulement en recette.
Exercices
1. Mesurer une base. Mesurez, pour node:24-slim et node:24-alpine, la taille compressée, le nombre de paquets et le nombre de vulnérabilités hautes et critiques corrigeables. Lesquelles viennent du système, lesquelles de npm ?
Solution
$ for i in node:24-slim node:24-alpine; do
docker image ls --tree $i | grep linux/amd64
docker save $i -o img.tar
docker run --rm -v "$PWD":/t:ro -v trivy-cache:/root/.cache aquasec/trivy:0.74.0 \
image --quiet --input /t/img.tar --scanners vuln --severity HIGH,CRITICAL --ignore-unfixed --format json \
| jq -r '.Results[] | select(.Vulnerabilities) | .Type as $ty | .Vulnerabilities[] | "\($ty) \(.PkgName)"' | sort | uniq -c
done
Le champ Type distingue les paquets du système (debian, alpine) des bibliothèques du langage (node-pkg). Au 1er octobre 2026, node:24-slim pèse 80,8 Mo compressés et node:24-alpine 61,6 Mo, et toutes leurs vulnérabilités hautes corrigeables sont identiques et de type node-pkg : brace-expansion (4), ip-address, tar et undici. Ce sont des dépendances de npm, embarqué dans l'image, exactement comme pip pour Python. Une image d'exécution sans npm s'en débarrasse. Les chiffres dépendent du jour de la mesure.
2. Le binaire mystère. Un collègue copie dans une image alpine:3.22 un binaire fourni par un éditeur, et obtient exec /opt/outil/agent: no such file or directory. Le fichier est bien là. Donnez la cause probable, la commande qui la confirme, et deux solutions.
Solution
Le binaire est lié dynamiquement contre glibc et cherche un chargeur (/lib64/ld-linux-x86-64.so.2) absent d'Alpine. file /opt/outil/agent (sur l'hôte) ou readelf -l /opt/outil/agent | grep interpreter l'affiche. Solutions : utiliser une image de base glibc (debian:13-slim, distroless cc, Wolfi) ; obtenir de l'éditeur une version statique ou compilée pour musl. Installer la couche de compatibilité gcompat d'Alpine dépanne parfois, mais n'est pas une solution de production.
3. Choisir pour trois applications (niveau 300). Proposez et justifiez une image de base pour : (a) Signalements ; (b) signalements-export, compilé en binaire statique ; (c) une application Java d'un éditeur, livrée en .jar, que le client veut faire tourner sur un socle certifié Red Hat.
Solution
(a) python:3.14-slim comme base d'exécution est un bon défaut (glibc, compatibilité maximale, correctifs Debian), à condition de sortir pip de l'image finale (leçon 2) ; python:3.14-alpine est un choix défendable puisque toutes les dépendances ont des roues musllinux et que l'application n'est pas gourmande en calcul, à valider par des tests ; distroless n'est possible que si l'on accepte Python 3.13. (b) scratch ou gcr.io/distroless/static-debian13:nonroot : un binaire statique n'a besoin de rien, sinon des certificats d'autorité, des données de fuseaux horaires et d'un utilisateur non-root, que l'image distroless fournit (leçon 5). (c) ubi9/openjdk-21-runtime ou ubi9/ubi-minimal avec un JDK : la contrainte de support Red Hat prime sur la taille. Dans tous les cas, documentez le choix et sa date de révision.
4. Lire un rapport (niveau 200). Pourquoi python:3.14-slim affiche-t-il 55 vulnérabilités hautes alors que debian:13-slim, sur lequel il est construit, en affiche 48 ? Identifiez la provenance des 7 supplémentaires.
Solution
Listez les vulnérabilités hautes des deux images dans deux fichiers triés, puis comparez-les avec comm :
$ comm -13 debian_13-slim.txt python_3.14-slim.txt
debian libncursesw6 CVE-2025-69720
debian openssl CVE-2026-75804
debian openssl CVE-2026-84782
python-pkg msgpack GHSA-6v7p-g79w-8964
python-pkg setuptools CVE-2025-47273
python-pkg urllib3 CVE-2026-97687
python-pkg urllib3 CVE-2026-97689
(Chaque fichier contient type paquet identifiant, produit par le filtre jq de la leçon.) Trois viennent de paquets Debian que l'image Python ajoute (ncurses pour l'interpréteur interactif, le paquet openssl complet), quatre des bibliothèques embarquées par pip. Ces quatre-là disparaissent d'une image d'exécution sans pip.
Récapitulatif
- L'image de base détermine l'essentiel de la taille, de la surface d'attaque, de la compatibilité et du rythme des correctifs de votre image.
- Mesurez :
docker image ls --treepour la taille, Trivy pour les paquets et les vulnérabilités. Raisonnez sur les vulnérabilités corrigeables, pas sur les nombres bruts, qui reflètent la politique de chaque distribution. - Les images de langage complètes (
python:3.14) servent à construire ; on exécute sur une variante slim, Alpine, distroless ou durcie. - glibc et musl ne sont pas interchangeables : un binaire dynamique glibc échoue sur Alpine avec un trompeur
no such file or directory. Les binaires statiques tournent sur tout Linux de la même architecture. - Alpine n'est pas forcément lent ni incompatible : mesurez votre charge et vérifiez la disponibilité des roues
musllinux. - Une image de base vieillit dès sa publication : reconstruisez régulièrement, standardisez vos bases, documentez le choix.
Pour aller plus loin
- La page Functional differences from glibc du wiki de musl, indispensable avant de passer une application sur Alpine.
- Le dépôt distroless de Google, qui explique ce que contient chaque variante (
static,base,cc, langages) et la différence entre les étiquetteslatest,nonrootetdebug. - La documentation de Chainguard et l'annonce des Docker Hardened Images, pour suivre l'évolution rapide de l'offre d'images durcies.
- Leçon suivante : les constructions multi-étapes, pour construire avec une image complète et exécuter avec une image minimale.
Sources
- Docker, Building best practices : choose the right base image
- Image officielle Python : variantes (slim, alpine)
- musl libc, Functional differences from glibc
- GoogleContainerTools, distroless
- Chainguard Academy, Overview of Chainguard Containers
- Docker, Docker Hardened Images for everyone (17 décembre 2025)
- Red Hat, Universal Base Images
- Debian Security Tracker, statuts des vulnérabilités
- Trivy, Vulnerability data sources