Votre image Docker fait 1 Go alors qu'elle pourrait en faire 50 Mo ? Ce guide vous accompagne pas à pas pour réduire la taille de vos images de 50 à 90%. Nous commencerons par comprendre pourquoi c'est crucial, puis nous appliquerons les techniques dans l'ordre d'impact : du plus efficace au plus fin.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Pourquoi optimiser : impact sur les coûts, la sécurité et les performances de déploiement
- Choisir son image de base : scratch, distroless, Alpine, Chainguard, quel choix pour quel usage ?
- Multi-stage build : séparer build et runtime pour exclure les outils de compilation
- Optimisations fines : .dockerignore, nettoyage des caches, réduction des couches
Pourquoi optimiser la taille de vos images ?
Section intitulée « Pourquoi optimiser la taille de vos images ? »Avant de plonger dans les techniques, comprenons pourquoi la taille des images est un enjeu critique. Une image de 1 Go au lieu de 50 Mo a des conséquences bien réelles.
Impact sur les coûts
Section intitulée « Impact sur les coûts »Le surcoût d'une image obèse ne se lit pas sur une facture unique : il se multiplie par le nombre de tags conservés dans le registry et par le nombre de pull déclenchés à chaque déploiement. Le tableau ci-dessous compare les deux extrêmes sur trois postes ; la colonne qui compte vraiment est la bande passante, car elle est facturée à chaque redémarrage de pod, pas une seule fois au push.
| Métrique | Image 1 Go | Image 50 Mo | Économie |
|---|---|---|---|
| Stockage registry (100 images) | 100 Go | 5 Go | 95% |
| Bande passante déploiement | 1 Go/nœud | 50 Mo/nœud | 95% |
| Temps de pull (100 Mbps) | ~80 sec | ~4 sec | 95% |
Sur un cluster Kubernetes de 50 nœuds avec des déploiements fréquents, ces chiffres se traduisent en milliers d'euros de coûts cloud évités.
Impact sur la sécurité
Section intitulée « Impact sur la sécurité »Chaque binaire embarqué dans l'image devient un outil disponible pour un attaquant qui obtient l'exécution de code dans le conteneur. Un scanner de vulnérabilités compte les paquets présents, pas ceux réellement utilisés : retirer un paquet inutile fait donc baisser le rapport de scan et la surface d'attaque réelle.
Une image volumineuse contient généralement :
- Des shells (
bash,sh) permettant l'exécution de commandes malveillantes - Des gestionnaires de paquets (
apt,yum) pour installer des backdoors - Des outils réseau (
curl,wget) pour exfiltrer des données
En production, vous n'avez besoin de rien de tout cela.
Impact sur les performances
Section intitulée « Impact sur les performances »Le temps de pull est le seul poste où la taille se voit à l'œil nu, mais il frappe au pire moment : pendant un scaling sous charge ou un rollback d'incident. Lisez le tableau ligne par ligne en vous demandant à chaque fois « combien de fois par jour cette phase se produit-elle chez moi ». Un pipeline qui builde vingt fois par jour paie vingt fois la différence.
| Phase | Image volumineuse | Image optimisée |
|---|---|---|
| Pull initial | Lent | Rapide |
| Scaling (nouveaux pods) | Attente du pull | Quasi-instantané |
| Rollback | Lent si ancienne image évincée | Rapide |
| CI/CD pipeline | Build + push longs | Cycles courts |
Identifier la taille de votre image actuelle
Section intitulée « Identifier la taille de votre image actuelle »Avant d'optimiser, mesurez. Les trois commandes ci-dessous répondent à trois
questions différentes : docker images donne le poids total,
docker history montre quelle instruction du Dockerfile a coûté cher, et
dive permet d'ouvrir une couche pour voir quels fichiers l'occupent.
Commencez toujours par docker history : la couche la plus lourde vous désigne
la ligne à réécrire.
# Taille des images localesdocker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
# Détail des couches d'une imagedocker history <image>:<tag>
# Analyse interactive avec Divedive <image>:<tag>Attention à la colonne que vous lisez. Sur Docker 29, l'affichage par défaut
de docker images sépare DISK USAGE (ce que l'image occupe réellement sur
le disque local) de CONTENT SIZE (le poids du contenu tel qu'il est stocké
et transféré, donc compressé). Les deux valeurs diffèrent d'un facteur 2 à 3 sur
une petite image : comparez toujours avant et après avec la même colonne.
Maintenant que vous comprenez les enjeux, passons aux techniques d'optimisation, dans l'ordre d'impact.
Choisissez la bonne image de base
Section intitulée « Choisissez la bonne image de base »Le choix de l'image de base impacte directement la taille et la sécurité de vos conteneurs. Une image standard Debian peut contenir des centaines de CVE, alors qu'une image Chainguard en a généralement zéro.
Comparatif des images de base
Section intitulée « Comparatif des images de base »Deux colonnes décident à elles seules de votre choix : Libc et Shell. La libc conditionne la compatibilité (un binaire compilé contre glibc ne démarrera pas sur une base musl), le shell conditionne la capacité à déboguer et, symétriquement, ce qu'un attaquant pourra lancer. La taille n'arrive qu'en troisième position : passer de 85 Mo à 9 Mo change peu de choses si votre application pèse 400 Mo à elle seule.
Les tailles ci-dessous sont les couches décompressées relevées avec
docker history sur l'architecture amd64 en juillet 2026. Elles bougent à
chaque publication amont : refaites la mesure avant de citer un chiffre.
| Image | Taille | CVE typiques | Shell | Libc | Cas d'usage |
|---|---|---|---|---|---|
scratch | 0 Mo | 0 | non | aucune | Binaires statiques Go/Rust |
gcr.io/distroless/static-debian12 | ~5 Mo | 0-5 | non | aucune | Binaires statiques Google |
cgr.dev/chainguard/static | ~6 Mo | 0 | non | aucune | Binaires statiques + certificats |
alpine:3.22 | ~9 Mo | 0-10 | oui | musl | Usage général léger |
cgr.dev/chainguard/wolfi-base | ~18 Mo | 0 | oui | glibc | Apps nécessitant glibc |
gcr.io/distroless/base-debian12 | ~25 Mo | 0-10 | non | glibc | Apps avec libc dynamique |
debian:bookworm-slim | ~85 Mo | 50-150 | oui | glibc | Compatibilité maximale |
ubuntu:24.04 | ~88 Mo | 50-200 | oui | glibc | Environnement familier |
L'image scratch : le minimum absolu
Section intitulée « L'image scratch : le minimum absolu »scratch est une image totalement vide (0 octet). Elle convient uniquement
aux binaires compilés statiquement qui n'ont pas besoin de libc :
# Pour Go avec compilation statiqueFROM golang:1.22-alpine AS builderWORKDIR /appCOPY . .RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/myapp
FROM scratchCOPY --from=builder /app/myapp /myappCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ENTRYPOINT ["/myapp"]Attention : sans certificats SSL, vos appels HTTPS échoueront. Copiez-les
depuis le builder ou utilisez distroless/static qui les inclut.
Images distroless : sécurité sans compromis
Section intitulée « Images distroless : sécurité sans compromis »Les images distroless (Google) et Chainguard ne contiennent ni shell,
ni gestionnaire de paquets. Un attaquant qui compromet votre conteneur ne peut
pas exécuter bash, wget ou installer d'outils.
# Application Java avec distrolessFROM eclipse-temurin:21-jdk AS builderWORKDIR /appCOPY . .RUN ./mvnw package -DskipTests
FROM gcr.io/distroless/java21-debian12:latest@sha256:f34fd3e4e2d7a246d764d0614f5e6ffb3a735930723fac4cfc25a72798950262COPY --from=builder /app/target/*.jar /app.jarENTRYPOINT ["java", "-jar", "/app.jar"]Pour déboguer en développement, utilisez le tag :debug qui ajoute un shell
BusyBox : gcr.io/distroless/java21-debian12:debug.
Alpine Linux : léger mais avec des pièges
Section intitulée « Alpine Linux : léger mais avec des pièges »Alpine utilise musl libc au lieu de glibc, ce qui peut causer des problèmes de compatibilité :
Exemple d'image Alpine optimisée. Épinglez la branche stable et non le tag
latest : chaque branche Alpine est maintenue deux ans, la 3.20 est sortie de
support le 1er avril 2026, la 3.22 est suivie jusqu'au 1er mai 2027.
FROM alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dceRUN apk --no-cache add curl ca-certificatesChainguard Wolfi : glibc sans la distribution complète
Section intitulée « Chainguard Wolfi : glibc sans la distribution complète »Wolfi est une distribution créée par Chainguard spécifiquement pour les conteneurs. Elle combine :
- glibc pour la compatibilité maximale
- Mises à jour de sécurité quotidiennes
- 0 CVE garanti à la publication
- Construction avec apko (déclaratif, reproductible)
Attention à une confusion fréquente : wolfi-base n'est pas une image
distroless. Elle embarque BusyBox (/bin/sh pointe dessus) et le
gestionnaire de paquets apk, ce qui en fait une excellente base de build
mais une base de runtime à surveiller. Pour la production, utilisez-la comme
étage de construction et copiez le résultat dans cgr.dev/chainguard/static ou
une image applicative Chainguard sans shell.
# Application Python avec ChainguardFROM cgr.dev/chainguard/python:latest-dev@sha256:7406826ac06aa5e5b9b010c82b3f56aed62946c7fb5c7d4dfba012b88a6570c5 AS builderWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt --target=/app/deps
FROM cgr.dev/chainguard/python:latest@sha256:b3d3fbb8b9fe48950bab73d49bffa7496ff6f8a46ba570b302fc366f1396011aWORKDIR /appCOPY --from=builder /app/deps /app/depsCOPY . .ENV PYTHONPATH=/app/depsENTRYPOINT ["python", "app.py"]Une contrainte à connaître avant d'adopter Chainguard : le catalogue public
de cgr.dev/chainguard/ ne publie que les tags latest et latest-dev. Les
tags versionnés relèvent de l'offre payante. Comme latest bouge chaque
jour, épinglez vos images de production par digest
(cgr.dev/chainguard/python@sha256:…) et laissez un outil de mise à jour de
dépendances proposer les bumps, sinon vos builds ne sont pas reproductibles.
Guide de choix par langage
Section intitulée « Guide de choix par langage »Ce tableau est un point de départ, pas une règle absolue : la contrainte
décisive reste la présence d'extensions natives dans vos dépendances. Un
projet Python qui n'installe que du code pur passe très bien sur Alpine ; dès
qu'apparaissent numpy, psycopg2 ou cryptography compilés, la base
glibc évite des heures de compilation. Vérifiez cette question avant de
choisir la ligne qui vous concerne.
| Langage | Image recommandée | Pourquoi |
|---|---|---|
| Go | scratch ou distroless/static | Binaire statique, 0 dépendance |
| Rust | scratch ou distroless/static | Compilation statique avec musl |
| Java | distroless/java21 ou chainguard/jre | JRE minimal, pas de shell |
| Python | chainguard/python ou python:3.12-slim | glibc pour les extensions natives |
| Node.js | chainguard/node ou node:20-slim | Évite les problèmes musl |
| .NET | mcr.microsoft.com/dotnet/runtime | Images Microsoft optimisées |
Utilisez le multi-stage build
Section intitulée « Utilisez le multi-stage build »Le multi-stage est une fonctionnalité introduite avec Docker 17.05 qui
permet de décrire plusieurs étapes dans un même Dockerfile. Chaque stage
commence par une instruction FROM.
Principe en 3 stages :
- Stage base : environnement commun (OS + dépendances runtime)
- Stage builder : compilation, téléchargement des dépendances, build
- Stage production : copie sélective uniquement du résultat final
Syntaxe clé :
- Nommez vos stages avec
AS <nom>:FROM alpine:3.18 AS builder - Copiez entre stages avec
COPY --from=<nom>:COPY --from=builder /app/dist /app
Exemple : une image Ansible construite en deux temps
Section intitulée « Exemple : une image Ansible construite en deux temps »Le cas d'Ansible illustre bien le gain, parce qu'installer Ansible avec pip
demande un compilateur C et des en-têtes de développement dont l'image
finale n'a aucun besoin. Les deux Dockerfile qui suivent produisent le même
outil ; seul le découpage change. Les tailles indiquées ont été mesurées sur une
base alpine:3.10.2, la version de l'époque : les valeurs absolues ne sont plus
reproductibles telles quelles aujourd'hui, c'est l'écart entre les deux qui
reste l'enseignement.
Une image non optimisée
Section intitulée « Une image non optimisée »Ce premier Dockerfile installe les dépendances de compilation, construit, puis
les désinstalle avec apk del. Le problème est invisible à la lecture : la
suppression a lieu dans un RUN séparé, donc les paquets restent stockés
dans la couche précédente. L'image finale les transporte sans jamais pouvoir
s'en servir.
# ❌ Contre-exemple : chaque RUN fige une couche, apk del ne récupère rienFROM alpine:3.10.2@sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb
RUN apk update && apk upgradeRUN apk add --no-cache python3 opensslRUN apk add --no-cache --virtual .build-deps python3-dev gcc ca-certificates libffi-dev openssl-dev build-baseRUN pip3 install --no-cache-dir pip==26.1.2 ansible==14.2.0RUN apk del .build-depsSi vous ne maîtrisez pas le gestionnaire de paquets apk, je vous recommande de lire mon guide sur apk pour mieux comprendre son fonctionnement.
On contrôle la taille de l'image :
docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEansible 0.1 5e43ab54b9a0 56 seconds ago 393MBLa même image en multi-stage
Section intitulée « La même image en multi-stage »La version ci-dessous isole la compilation dans un stage builder et
n'emporte, dans l'image finale, que le virtualenv produit. Les en-têtes,
gcc et build-base restent dans un stage jamais publié : ils n'existent plus
du tout dans le résultat, contrairement au apk del précédent qui se contentait
de les masquer.
FROM alpine:3.10.2 AS baseRUN apk update && apk --no-cache upgrade && apk add --no-cache python3 openssl
FROM base AS builderRUN apk add --no-cache --virtual .build-deps python3-dev gcc ca-certificates libffi-dev openssl-dev build-base \ && python3 -m venv /opt/venv# On force l'utilisation du virtualenv pour les commandes suivantesENV PATH="/opt/venv/bin:$PATH"RUN pip3 install --no-cache-dir ansible==14.2.0
FROM base
COPY --from=builder /opt/venv /opt/venvENV PATH="/opt/venv/bin:$PATH"Le mot-clé AS s'écrit en majuscules. BuildKit accepte as mais lève un
avertissement FromAsCasing à chaque build ; docker build --check le
signale avant même de construire quoi que ce soit.
On contrôle :
docker images
REPOSITORY TAG IMAGE ID CREATED SIZEansible 0.2 304de2e8b235 51 seconds ago 165MBL'image ne fait plus que 165 Mo contre 393 Mo pour la version non optimisée, soit 58% de gain sans toucher une seule ligne du code applicatif.
Cette approche est applicable à d'autres langages et plateformes, permettant d'optimiser les images de conteneurs pour la production, peu importe que vous utilisiez Docker, Podman ou un autre moteur de conteneurisation, voire un orchestrateur tel que Kubernetes.
Utilisez .dockerignore
Section intitulée « Utilisez .dockerignore »Un fichier .dockerignore joue un rôle important dans l'optimisation de vos
images de conteneurs en excluant les fichiers et répertoires inutiles lors de la
construction de l’image. Tout comme un fichier .gitignore, il empêche
l'inclusion de fichiers temporaires, de configurations locales, ou de
dépendances non essentielles dans l'image finale, ce qui permet de réduire sa
taille de manière significative.
Par exemple, si votre projet contient des fichiers comme .env, node_modules,
ou des dossiers de test, vous pouvez les exclure facilement avec un
.dockerignore bien configuré :
Exemple :
# ignore all markdown files (md) beside all README*.md other than README-secret.md*.md!README*.mdREADME-secret.mdCela garantit que ces fichiers ne seront pas copiés dans le contexte de construction, réduisant ainsi la taille de l'image et évitant les risques liés à la fuite d'informations sensibles. En ne copiant que les fichiers nécessaires, votre image devient non seulement plus légère, mais aussi plus sécurisée.
Optimisez vos installations de paquets
Section intitulée « Optimisez vos installations de paquets »Une fois votre image de base choisie et votre multi-stage configuré, affinez les installations de paquets pour grappiller les derniers Mo.
Évitez les paquets inutiles
Section intitulée « Évitez les paquets inutiles »Chaque paquet installé ajoute du poids et des vulnérabilités potentielles. N'installez que le strict nécessaire pour l'exécution :
Sur Debian/Ubuntu : --no-install-recommends
Section intitulée « Sur Debian/Ubuntu : --no-install-recommends »Par défaut, apt-get installe les paquets "recommandés" qui ne sont pas
toujours nécessaires. Bloquez ce comportement :
RUN apt-get update && apt-get install -y --no-install-recommends nginx \ && rm -rf /var/lib/apt/lists/*Important : supprimez toujours /var/lib/apt/lists/* dans le même RUN
pour ne pas conserver les 30-50 Mo de listes de paquets.
Sur Alpine : --no-cache et --virtual
Section intitulée « Sur Alpine : --no-cache et --virtual »Alpine expose deux options qui répondent à des problèmes différents.
--no-cache empêche apk d'écrire l'index des dépôts dans
/var/cache/apk/, ce qui évite le couple apk update puis rm -rf.
--virtual regroupe plusieurs paquets sous une étiquette artificielle, pour
pouvoir les retirer d'un seul geste. Les deux ne se remplacent pas : la
première concerne les métadonnées, la seconde les dépendances de compilation.
# --no-cache évite de stocker l'index des paquetsRUN apk add --no-cache curl ca-certificates
# --virtual pour les dépendances de build temporairesRUN apk add --no-cache --virtual .build-deps gcc make musl-dev \ && make \ && apk del .build-depsL'option --virtual crée un méta-paquet que vous pouvez supprimer en une
commande, emportant toutes ses dépendances.
Limitez le nombre de couches
Section intitulée « Limitez le nombre de couches »Chaque instruction RUN, COPY, ou ADD dans un fichier de construction comme
un Dockerfile crée une nouvelle couche dans l’image de conteneur. Plus vous avez
de couches, plus l'image devient volumineuse et complexe. Pour réduire la taille
de l'image, il est donc essentiel de combiner les commandes là où c'est
possible.
Par exemple, au lieu de séparer les commandes RUN en plusieurs lignes, vous
pouvez les combiner en une seule instruction :
RUN apt-get update && apt-get install -y --no-install-recommends curl \ && apt-get clean && rm -rf /var/lib/apt/lists/*Dans cet exemple, les commandes sont combinées en une seule instruction RUN,
ce qui crée une seule couche au lieu de plusieurs. Cette pratique permet non
seulement de réduire la taille finale de l'image, mais aussi de simplifier sa
gestion et son analyse.
Une autre bonne pratique consiste à supprimer les fichiers temporaires ou les caches à la fin de la commande combinée pour s'assurer qu'ils ne sont pas inclus dans l'image finale. Ainsi, en regroupant les commandes et en nettoyant après chaque installation, vous obtenez une image de conteneur plus légère, ce qui améliore la vitesse de déploiement et réduit l'utilisation des ressources, peu importe le moteur de conteneur utilisé (Docker, Podman, etc.).
Cette technique est essentielle pour maintenir des images efficaces et éviter l'accumulation de données inutiles au fil des modifications apportées à vos conteneurs.
Analysez vos images
Section intitulée « Analysez vos images »Analyser vos images de conteneurs est une étape importante pour identifier les opportunités d'optimisation. Des outils comme Dive vous permettent de visualiser chaque couche de votre image et de comprendre exactement ce qui contribue à sa taille. Dive vous aide à repérer les fichiers ou les couches redondantes, à voir où l'espace est gaspillé et à prendre des décisions éclairées pour réduire l'image.
Par exemple, après avoir construit une image, vous pouvez exécuter Dive pour analyser les couches :
dive <nom_de_l_image>Dive affichera une vue interactive où chaque couche est détaillée, avec la possibilité de voir quels fichiers ont été ajoutés ou modifiés à chaque étape. Cela vous permet d'identifier les couches inutiles ou surdimensionnées, qui peuvent être consolidées ou retirées dans une prochaine version de votre Dockerfile.

En appliquant ces analyses, vous pouvez affiner votre processus de construction d'images pour obtenir des conteneurs plus légers et performants. Ces optimisations contribuent directement à des déploiements plus rapides et à une utilisation plus efficace des ressources dans vos environnements de production.
À retenir
Section intitulée « À retenir »Techniques d'optimisation
Section intitulée « Techniques d'optimisation »- Multi-stage build : LA technique la plus efficace, réduction de 50-80%
- --no-install-recommends : évite les dépendances inutiles sur Debian/Ubuntu
- --no-cache sur Alpine : ne conserve pas le cache apk
- Combinez les RUN : moins de couches = image plus légère
- .dockerignore : exclut node_modules, .git, fichiers de test
- Analysez avec Dive : visualisez chaque couche pour optimiser
Choix de l'image de base
Section intitulée « Choix de l'image de base »| Priorité | Image | Taille | Sécurité |
|---|---|---|---|
| 1. Sécurité max | Chainguard static | ~6 Mo | 0 CVE |
| 2. Équilibre | distroless | ~5-25 Mo | 0-10 CVE |
| 3. Compatibilité | Alpine | ~9 Mo | 0-10 CVE (musl) |
| 4. Débutant | Debian slim | ~85 Mo | 50-150 CVE |
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Ce quiz porte sur les arbitrages du guide plutôt que sur la syntaxe : ordre
d'impact des techniques, effet réel d'un RUN séparé, choix de base selon la
libc. Une mauvaise réponse pointe vers la section à relire.
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)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications