Aller au contenu
Conteneurs & Orchestration medium

Optimiser la taille des images de container

23 min de lecture

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.

  • 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

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.

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étriqueImage 1 GoImage 50 MoÉconomie
Stockage registry (100 images)100 Go5 Go95%
Bande passante déploiement1 Go/nœud50 Mo/nœud95%
Temps de pull (100 Mbps)~80 sec~4 sec95%

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.

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.

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.

PhaseImage volumineuseImage optimisée
Pull initialLentRapide
Scaling (nouveaux pods)Attente du pullQuasi-instantané
RollbackLent si ancienne image évincéeRapide
CI/CD pipelineBuild + push longsCycles courts

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.

Fenêtre de terminal
# Taille des images locales
docker images --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"
# Détail des couches d'une image
docker history <image>:<tag>
# Analyse interactive avec Dive
dive <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.

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.

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.

ImageTailleCVE typiquesShellLibcCas d'usage
scratch0 Mo0nonaucuneBinaires statiques Go/Rust
gcr.io/distroless/static-debian12~5 Mo0-5nonaucuneBinaires statiques Google
cgr.dev/chainguard/static~6 Mo0nonaucuneBinaires statiques + certificats
alpine:3.22~9 Mo0-10ouimuslUsage général léger
cgr.dev/chainguard/wolfi-base~18 Mo0ouiglibcApps nécessitant glibc
gcr.io/distroless/base-debian12~25 Mo0-10nonglibcApps avec libc dynamique
debian:bookworm-slim~85 Mo50-150ouiglibcCompatibilité maximale
ubuntu:24.04~88 Mo50-200ouiglibcEnvironnement familier

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 statique
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/myapp
FROM scratch
COPY --from=builder /app/myapp /myapp
COPY --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.

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 distroless
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY . .
RUN ./mvnw package -DskipTests
FROM gcr.io/distroless/java21-debian12:latest@sha256:f34fd3e4e2d7a246d764d0614f5e6ffb3a735930723fac4cfc25a72798950262
COPY --from=builder /app/target/*.jar /app.jar
ENTRYPOINT ["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 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:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
RUN apk --no-cache add curl ca-certificates

Chainguard 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 Chainguard
FROM cgr.dev/chainguard/python:latest-dev@sha256:7406826ac06aa5e5b9b010c82b3f56aed62946c7fb5c7d4dfba012b88a6570c5 AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt --target=/app/deps
FROM cgr.dev/chainguard/python:latest@sha256:b3d3fbb8b9fe48950bab73d49bffa7496ff6f8a46ba570b302fc366f1396011a
WORKDIR /app
COPY --from=builder /app/deps /app/deps
COPY . .
ENV PYTHONPATH=/app/deps
ENTRYPOINT ["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.

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.

LangageImage recommandéePourquoi
Goscratch ou distroless/staticBinaire statique, 0 dépendance
Rustscratch ou distroless/staticCompilation statique avec musl
Javadistroless/java21 ou chainguard/jreJRE minimal, pas de shell
Pythonchainguard/python ou python:3.12-slimglibc pour les extensions natives
Node.jschainguard/node ou node:20-slimÉvite les problèmes musl
.NETmcr.microsoft.com/dotnet/runtimeImages Microsoft optimisées

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 du multi-stage build

Principe en 3 stages :

  1. Stage base : environnement commun (OS + dépendances runtime)
  2. Stage builder : compilation, téléchargement des dépendances, build
  3. 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.

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 rien
FROM alpine:3.10.2@sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb
RUN apk update && apk upgrade
RUN apk add --no-cache python3 openssl
RUN apk add --no-cache --virtual .build-deps python3-dev gcc ca-certificates libffi-dev openssl-dev build-base
RUN pip3 install --no-cache-dir pip==26.1.2 ansible==14.2.0
RUN apk del .build-deps

Si 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 :

Fenêtre de terminal
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ansible 0.1 5e43ab54b9a0 56 seconds ago 393MB

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 base
RUN apk update && apk --no-cache upgrade && apk add --no-cache python3 openssl
FROM base AS builder
RUN 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 suivantes
ENV PATH="/opt/venv/bin:$PATH"
RUN pip3 install --no-cache-dir ansible==14.2.0
FROM base
COPY --from=builder /opt/venv /opt/venv
ENV 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 :

Fenêtre de terminal
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ansible 0.2 304de2e8b235 51 seconds ago 165MB

L'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.

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*.md
README-secret.md

Cela 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.

Une fois votre image de base choisie et votre multi-stage configuré, affinez les installations de paquets pour grappiller les derniers Mo.

Chaque paquet installé ajoute du poids et des vulnérabilités potentielles. N'installez que le strict nécessaire pour l'exécution :

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.

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 paquets
RUN apk add --no-cache curl ca-certificates
# --virtual pour les dépendances de build temporaires
RUN apk add --no-cache --virtual .build-deps gcc make musl-dev \
&& make \
&& apk del .build-deps

L'option --virtual crée un méta-paquet que vous pouvez supprimer en une commande, emportant toutes ses dépendances.

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.

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 :

Fenêtre de terminal
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.

dive

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.

  1. Multi-stage build : LA technique la plus efficace, réduction de 50-80%
  2. --no-install-recommends : évite les dépendances inutiles sur Debian/Ubuntu
  3. --no-cache sur Alpine : ne conserve pas le cache apk
  4. Combinez les RUN : moins de couches = image plus légère
  5. .dockerignore : exclut node_modules, .git, fichiers de test
  6. Analysez avec Dive : visualisez chaque couche pour optimiser
PrioritéImageTailleSécurité
1. Sécurité maxChainguard static~6 Mo0 CVE
2. Équilibredistroless~5-25 Mo0-10 CVE
3. CompatibilitéAlpine~9 Mo0-10 CVE (musl)
4. DébutantDebian slim~85 Mo50-150 CVE

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

15 questions
8 min.
80% requis

Informations

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

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn