Retour au cours

infra / docker

Optimisation d'images (layers, cache, taille, distroless)

Leçon 101 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le mécanisme du cache de build et ordonner un Dockerfile pour en tirer parti
  • Comprendre pourquoi une suppression dans un RUN séparé n'allège jamais une image
  • Combiner installation et nettoyage dans une seule instruction RUN pour réduire réellement la taille
  • Choisir une image de base adaptée (complète, slim, alpine, distroless) selon le compromis recherché
  • Mesurer et comparer la taille réelle d'une image avec docker history et des outils dédiés comme dive

Dans quel contexte ?

Une équipe remarque que son pipeline de déploiement met 8 minutes à transférer chaque nouvelle image vers le serveur de production, ralentissant considérablement chaque mise à jour. En inspectant l'image avec dive, elle découvre qu'une couche à elle seule pèse 400 Mo à cause d'un apt-get install suivi d'un rm -rf dans deux instructions RUN séparées — les fichiers supprimés existaient toujours dans la couche précédente. En fusionnant les deux dans une seule instruction, l'image perd son surpoids inutile.

Pourquoi la taille d'une image compte

Une image plus légère se télécharge plus vite, se déploie plus vite, et offre moins de surface d'attaque à un éventuel intrus. Optimiser une image, c'est appliquer plusieurs leviers complémentaires, dont certains sont presque gratuits une fois qu'on en a compris la logique.

Image de baseTaille approx.Compromis
python:3.12~1 GoComplet, lent à transférer/déployer
python:3.12-slim~150 MoBon équilibre, recommandé par défaut
python:3.12-alpine~50 MoTrès léger, compatibilité C/musl à vérifier
distroless~50-80 MoMinimal et sécurisé, débogage plus difficile

Piège fréquent

Faire un apt-get install puis un rm dans deux instructions RUN séparées n'allège rien : chaque couche créée reste inscrite dans l'image, même si une instruction ultérieure supprime des fichiers qu'elle contenait. Installer ET nettoyer dans la MÊME instruction RUN est indispensable pour que le nettoyage ait un effet réel sur la taille finale.

Comprendre le cache de build, étape par étape

À chaque instruction d'un Dockerfile, Docker vérifie si le résultat est déjà en cache. Dès qu'une instruction change (ou que son contenu d'entrée change), TOUTES les instructions suivantes doivent être rejouées, même si elles n'ont elles-mêmes pas changé. La conséquence pratique : il faut ordonner le Dockerfile du plus stable vers le plus volatile. Copier d'abord le fichier de dépendances (qui change rarement) et installer les dépendances, puis copier le reste du code (qui change à chaque modification), permet de garder le cache d'installation valide la plupart du temps, au lieu de tout réinstaller à chaque build.

Une couche, ce n'est pas gratuit

Chaque couche créée reste inscrite dans l'image, même si une instruction ultérieure supprime des fichiers qu'elle contenait. Faire un apt-get install puis un rm dans deux instructions RUN séparées n'allège donc rien : les fichiers supprimés existent toujours dans la couche précédente. Il faut installer ET nettoyer dans la MÊME instruction RUN pour que le nettoyage ait un effet réel sur la taille finale.

Choisir sa base avec discernement

Une image de base complète (python:3.12) pèse souvent dix fois plus qu'une version "slim", elle-même plus lourde qu'une version Alpine. Alpine utilise une bibliothèque C différente (musl au lieu de glibc), ce qui peut casser certaines dépendances compilées — un compromis à connaître avant de choisir aveuglément la plus petite image. Les images "distroless" vont encore plus loin : ni shell, ni gestionnaire de paquets, ce qui renforce la sécurité au prix d'un débogage plus difficile.

Commandes & code

Optimisation d'images

dockerfile
# --- Ordre des instructions : maximiser le cache de build ---
# MAUVAIS : le code change souvent -> invalide le cache de "pip install" à CHAQUE build
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

# BON : requirements.txt change rarement -> le cache de pip install est réutilisé la plupart du temps
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
# Docker invalide le cache d'une couche dès que son contenu (ou celui d'une couche précédente) change
dockerfile
# --- Réduire le nombre et le poids des couches ---
# MAUVAIS : chaque RUN crée une couche, "rm" dans un RUN séparé n'allège rien (le fichier a déjà été committé)
RUN apt-get update
RUN apt-get install -y curl git
RUN rm -rf /var/lib/apt/lists/*

# BON : une seule couche, le nettoyage a lieu AVANT la fin de la couche
RUN apt-get update && apt-get install -y --no-install-recommends curl git \
    && rm -rf /var/lib/apt/lists/*
dockerfile
# --- Choisir une image de base légère ---
FROM python:3.12              # ~1 Go, inclut beaucoup d'outils inutiles en prod
FROM python:3.12-slim           # ~150 Mo, Debian minimal
FROM python:3.12-alpine           # ~50 Mo, mais libc différente (musl) : peut casser des libs C compilées

# --- distroless : encore plus minimal, pas même de shell ni de gestionnaire de paquets ---
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /app /app
CMD ["app/main.py"]
# Avantage sécurité : sans shell, un attaquant qui compromet l'appli ne peut pas obtenir de shell interactif
bash
# Mesurer et comparer la taille réelle des images
docker images mon-api
docker history mon-api:1.0 --no-trunc      # détail du poids de chaque couche

# Outil dédié à l'analyse de couches (dive)
dive mon-api:1.0             # explore interactivement chaque couche, repère le gaspillage

# Squash : fusionne toutes les couches en une seule (perd le bénéfice du cache incrémental, à réserver à la release finale)
docker build --squash -t mon-api:1.0-squashed .

# Vérifier qu'aucun fichier sensible/inutile n'a fui dans l'image
docker run --rm mon-api:1.0 find / -name "*.pyc" 2>/dev/null | head
docker run --rm mon-api:1.0 du -sh /app
Image de baseTaille approx.Compromis
python:3.12~1 Gocomplet, lent à transférer/déployer
python:3.12-slim~150 Mobon équilibre, recommandé par défaut
python:3.12-alpine~50 Motrès léger, compatibilité C/musl à vérifier
distroless~50-80 Mominimal + sécurisé, debug plus difficile (pas de shell)

Résumé

  • Ordonner les instructions du plus stable au plus volatile maximise la réutilisation du cache de build.
  • Combiner RUN apt-get install && rm -rf /var/lib/apt/lists/* en une seule couche évite d'alourdir l'image pour rien.
  • slim/alpine/distroless réduisent drastiquement la taille et la surface d'attaque par rapport à une image complète.

Exercices pratiques

1 disponible
1

Mission : traquer une couche de 400 Mo gaspillée dans le pipeline de déploiement

Objectif : Corriger un Dockerfile qui gaspille de l'espace à cause de RUN séparés, et choisir une base adaptée sans casser l'application.

Contexte

dive révèle qu'une couche de l'image mon-api pèse à elle seule 400 Mo, à cause d'un apt-get install suivi d'un rm -rf dans deux instructions RUN séparées. L'équipe envisage aussi de passer l'image de base en alpine pour gagner encore plus de place.

Résoudre l’exercice →