Vos images Docker pèsent souvent dix fois plus que votre application réelle, et docker images ne vous dit pas pourquoi.
Une image Docker fonctionne comme un mille-feuille. Une layer (une couche), c'est ce qu'ajoute chaque instruction de votre Dockerfile : un RUN, un COPY, un ADD. Chaque couche est posée par-dessus la précédente, et tout ce que vous écrivez dans une couche y reste , même si la couche suivante le supprime. C'est pour ça qu'une image finit à 1,5 Go alors que votre application tient en 80 Mo : vous trimballez des caches, des fichiers de build et des dépendances qui n'auraient jamais dû quitter votre machine de dev.
Dive est l'outil qui ouvre une image et vous montre, couche par couche, ce qu'il y a dedans. Il calcule un efficiency score - le pourcentage de fichiers réellement utiles, et pointe les fichiers ajoutés puis effacés, ceux qui pèsent pour rien. Une fois Dive en main, vous aurez analysé une vraie image, identifié les trois sources de gaspillage les plus fréquentes, et appliqué les corrections qui réduisent la taille de 30 à 70 % sans changer une ligne de votre code.
Dive est distribué sous forme de binaire, un seul fichier exécutable, sans installation système. Sur macOS via Homebrew :
brew install diveSur Linux (Debian/Ubuntu), via le paquet .deb officiel :
DIVE_VERSION=$(curl -sL "https://api.github.com/repos/wagoodman/dive/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
Si vous ne voulez rien installer, lancez Dive directement via Docker :
docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
wagoodman/dive:latest <votre-image>:<tag>Une fois installé, prenez n'importe quelle image locale et lancez :
dive node:20Dive télécharge l'image si nécessaire, l'inspecte, puis ouvre une interface en deux panneaux. À gauche : la liste des layers avec leur taille. À droite : l'arborescence des fichiers de la couche sélectionnée. En bas, un encart résume l'image entière :

Deux indicateurs comptent ici :
Premier réflexe qui surprend : Dive ne réagit qu'au clavier, pas à la souris. Naviguez avec les flèches. Tab passe d'un panneau à l'autre. Ctrl+Space plie l'arborescence. Et la touche la plus utile, c'est Ctrl+U : elle filtre l'arborescence pour ne montrer que les fichiers ajoutés ou modifiés par la couche sélectionnée. Vous voyez immédiatement ce que cette instruction du Dockerfile a réellement coûté.
À retenir : lancez dive <image>, lisez le score en bas, ouvrez les couches lourdes avec Ctrl+U. C'est le geste de base, à répéter sur toutes vos images.
Avant de regarder la taille, vous pouvez aussi linter votre Dockerfile avec Dockle pour repérer les anti-patterns structurels. Ensuite, ouvrez une image qui vous appartient. Soyons honnête : sur les images officielles à jour (node:20-slim, python:3.12-slim), vous gagnerez peu, elles sont déjà optimisées. Le vrai gain est sur vos images, où le gaspillage s'accumule au fil des PR. Voici les trois sources que Dive met en évidence le plus souvent.
Sur une image Debian, un apt-get install sans nettoyage laisse traîner /var/lib/apt/lists/ (souvent 30 à 50 Mo). Dans Dive, ces fichiers apparaissent en jaune dans la couche du RUN apt-get. La correction tient sur une ligne :
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*Le point critique, c'est le && : tout doit se faire dans la même instruction RUN. Si vous nettoyez dans un RUN séparé, la couche précédente garde quand même les fichiers, Docker ne peut pas supprimer rétroactivement ce qui est déjà committé dans une couche. C'est ce piège-là qui fait que beaucoup de Dockerfiles « avec nettoyage » ne nettoient en fait rien du tout.
Regardez la couche du COPY . . : si vous voyez node_modules, .git, un dist/ issu d'un build local, ou des fichiers .env, vous copiez tout votre dossier de travail dans l'image. La correction passe par un fichier .dockerignore à la racine du projet :
.git
node_modules
dist
*.log
.env
.env.*
coverageRebuild, relancez dive : la couche du COPY doit être nettement plus légère, et le score remonte. Sur un projet Node typique, c'est souvent le gain le plus rapide, quelques centaines de mégaoctets en moins pour deux minutes de travail.
Si votre image contient à la fois gcc, make, vos sources et le binaire compilé, vous trimballez la chaîne de compilation jusqu'en production. Dive le rend évident : la couche d'installation des outils de build pèse souvent plus que l'application elle-même. La solution, c'est le multi-stage build : un Dockerfile à plusieurs étapes FROM, où seule la dernière finit dans l'image publiée.
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# image finale
FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --omit=dev
CMD ["node", "dist/server.js"]Seule l'étape FROM node:20-slim finit dans l'image publiée. gcc, les sources, les dépendances de dev et le cache npm du builder restent dans une étape intermédiaire jamais distribuée. En passant en plus de node:20 (~1,1 Go) à node:20-slim (~240 Mo) comme base finale, vous gagnez encore plusieurs centaines de mégaoctets.
Pour valider chaque modification sans devoir relancer Dive à la main, l'outil a un mode CI prévu pour ça :
CI=true dive <votre-image>:<tag> --highestUserWastedPercent 0.05Cette commande échoue (code de sortie non nul) si plus de 5 % de l'image est du gaspillage. C'est ce qu'on branche dans une pipeline GitLab CI ou GitHub Actions, les deux plateformes les plus courantes de CI/CD (l'enchaînement automatisé de tests et de déploiements), pour empêcher qu'une image qui régresse côté taille passe en production sans qu'on le voie.
Une fois les trois patterns ci-dessus appliqués, le palier suivant, c'est l'image distroless. Une image distroless, c'est une image qui ne contient que votre application et le runtime nécessaire pour l'exécuter - pas de shell, pas de gestionnaire de paquets, pas même ls ou cat. C'est Google qui maintient les principales (gcr.io/distroless/...), et l'objectif est simple : tout ce qui n'est pas votre code n'a pas à être là.
Le multi-stage build du chapitre précédent devient :
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# image finale
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
CMD ["dist/server.js"]Sur un projet Node, l'image finale tombe typiquement de ~240 Mo (node:20-slim) à ~150 Mo (distroless/nodejs20). Lancez dive sur les deux versions pour mesurer le gain réel sur votre application.
Soyons honnête sur le compromis : debugger un conteneur distroless est plus difficile. Vous ne pouvez pas faire docker exec -it <container> bash pour fouiller, il n'y a pas de bash. Pas de curl, pas de ps, pas de vi. Si votre prod plante à 3 h du matin et que votre seul réflexe est d'entrer dans le conteneur en mode interactif, ça va piquer. Deux contournements existent :
:debug (par ex. gcr.io/distroless/nodejs20-debian12:debug) qui inclut un shell busybox minimal.Concrètement : passez en distroless une fois que les trois patterns précédents sont en place et que votre observabilité tient la route. Pas avant. Vous gagnez en taille et en surface d'attaque, mais vous ne gagnez rien si chaque incident vous force à rebuilder une variante :debug pour pouvoir investiguer.
Dive ne réduit pas vos images automatiquement. Il vous montre où se cache le gaspillage pour que vous puissiez agir au bon endroit. Le réflexe à prendre : avant de pousser une image en production, lancez dive dessus, regardez le score, ouvrez les couches les plus lourdes avec Ctrl+U et demandez-vous est-ce que ce fichier doit vraiment être là ? Dans la majorité des cas, vous trouverez en quelques minutes un cache apt, un node_modules copié par erreur ou des outils de build oubliés.
Pour aller plus loin, branchez Dive en mode CI sur un seuil que votre équipe accepte (5 % est un bon point de départ), passez vos Dockerfiles en multi-stage build, et regardez les images de base distroless ou Alpine quand vos dépendances le permettent. Une image bien optimisée, ce n'est pas qu'un confort : c'est moins de bande passante au pull, des déploiements plus rapides, et une surface d'attaque plus réduite côté sécurité, complétez ce travail en scannant vos images Docker avec Trivy. Et si vous hésitez encore sur le runtime de conteneurs, voyez le comparatif Docker vs Podman.
Merci de me suivre dans cette aventure ! 🍺