Docker pour les devs - 05 - Layers et cache

Comment les couches Docker fonctionnent, pourquoi l'ordre des instructions compte, et comment exploiter le cache.

  1. 01 Docker pour les devs - 00 - Pourquoi Docker change tout
  2. 02 Docker pour les devs - 01 - Containers vs VMs
  3. 03 Docker pour les devs - 02 - L'architecture de Docker
  4. 04 Docker pour les devs - 03 - Docker Desktop, Engine et alternatives
  5. 05 Docker pour les devs - 04 - Écrire un Dockerfile
  6. 06 Docker pour les devs - 05 - Layers et cache
  7. 07 Docker pour les devs - 06 - Le .dockerignore
  8. 08 Docker pour les devs - 07 - Multi-stage builds
  9. 09 Docker pour les devs - 08 - Choisir son image de base
  10. 10 Docker pour les devs - 09 - Docker Compose, les bases
  11. 11 Docker pour les devs - 10 - Docker Compose avance
  12. 12 Docker pour les devs - 11 - Networking Docker, les bases
  13. 13 Docker pour les devs - 12 - Networking Docker avance
  14. 14 Docker pour les devs - 13 - Volumes et persistance
  15. 15 Docker pour les devs - 14 - Variables d'environnement et secrets
  16. 16 Docker pour les devs - 15 - Permissions et utilisateurs
  17. 17 Docker pour les devs - 16 - Docker et monorepo
  18. 18 Docker pour les devs - 17 - Dev vs Prod
  19. 19 Docker pour les devs - 18 - ENTRYPOINT, CMD et scripts d'initialisation
  20. 20 Docker pour les devs - 19 - Debugger ses conteneurs
  21. 21 Docker pour les devs - 20 - Bases de donnees dans Docker
  22. 22 Docker pour les devs - 21 - Sauvegardes et restauration
  23. 23 Docker pour les devs - 22 - Conteneuriser un frontend
  24. 24 Docker pour les devs - 23 - Sécurité des conteneurs
  25. 25 Docker pour les devs - 24 - Optimisation des images
  26. 26 Docker pour les devs - 25 - Builds multi-platform
  27. 27 Docker pour les devs - 26 - Limiter les ressources de tes conteneurs
  28. 28 Docker pour les devs - 27 - Gerer les logs comme un adulte
  29. 29 Docker pour les devs - 28 - Healthchecks et restart policies
  30. 30 Docker pour les devs - 29 - Nettoyer Docker avant qu'il mange ton disque
  31. 31 Docker pour les devs - 30 - Registries et stratégie de tags
  32. 32 Docker pour les devs - 31 - Docker en CI/CD
  33. 33 Docker pour les devs - 32 - Au-dela de Compose
  34. 34 Docker pour les devs - 33 - Glossaire Docker de A a Z

05 - Layers et cache

Ce que tu vas apprendre

  • Comment chaque instruction créé une couche dans l'image
  • Les regles du cache Docker et quand il est invalide
  • L'astuce du COPY package.json avant COPY .
  • Comment analyser les couches avec docker history
  • Comment réduire la taille de tes images en comprenant les couches

Prerequisites


3 minutes vs 45 secondes

J'avais une image qui prenait 3 minutes a builder. A chaque modification d'une ligne de code, tout etait recalcule : installation des dépendances, compilation, tout. Apres avoir reordonne les instructions du Dockerfile, le build est passe a 45 secondes. Le code n'a pas change. Juste l'ordre.

Comprendre les couches, c'est comprendre pourquoi l'ordre des instructions dans un Dockerfile a un impact direct sur la vitesse de tes builds.

Chaque instruction créé une couche

Quand Docker exécuté un Dockerfile, chaque instruction (FROM, RUN, COPY, ADD) créé une couche. Une couche est un diff du système de fichiers : les fichiers ajoutes, modifies, ou supprimes par rapport a la couche précédente.

dockerfileFROM node:22-slim          # Couche 1 : l'image de base
WORKDIR /app               # Couche 2 : cree /app
COPY package.json ./       # Couche 3 : ajoute package.json
RUN npm ci                 # Couche 4 : ajoute node_modules
COPY . .                   # Couche 5 : ajoute le code source
CMD ["node", "index.js"]   # Metadata (pas une couche)

Les couches sont empilees. L'image finale est l'union de toutes les couches. Le mecanisme s'appelle un union filesystem (overlay2 sur la plupart des systèmes).

Les instructions qui ne modifient pas le système de fichiers (CMD, EXPOSE, ENV, LABEL) ne creent pas de couche. Elles ajoutent des metadonnees a l'image.

Comment le cache fonctionne

Docker cache chaque couche. Au prochain build, il compare chaque instruction avec le cache :

  1. L'instruction est-elle identique a la dernière fois ?
  2. Pour COPY/ADD : les fichiers copies ont-ils change (checksum) ?
  3. Pour RUN : la commande est-elle identique (texte brut) ?

Si la réponse est "rien n'a change", Docker réutilisé la couche en cache. Sinon, il reconstruit cette couche et toutes les couches suivantes.

C'est la regle fondamentale : une couche invalidee invalide toutes les couches qui viennent apres.

L'erreur classique

dockerfileFROM node:22-slim
WORKDIR /app
COPY . .                   # Tout le code source
RUN npm ci                 # Installation des deps
CMD ["node", "index.js"]

Le problème : tu changes une ligne dans index.js. Docker voit que COPY . . a change (le checksum est différent). Il invalide cette couche et tout ce qui suit. npm ci est relance, meme si package.json n'a pas bouge.

La solution : copier les dépendances d'abord

dockerfileFROM node:22-slim
WORKDIR /app

# Etape 1 : fichiers de dependances uniquement
COPY package.json package-lock.json ./

# Etape 2 : installation des dependances
RUN npm ci

# Etape 3 : code source
COPY . .

CMD ["node", "index.js"]

Maintenant, quand tu changes index.js :

  • COPY package.json package-lock.json ./ : pas change, cache utilise
  • RUN npm ci : instruction identique, couche précédente en cache, cache utilise
  • COPY . . : change, couche reconstruite

L'installation des dépendances est skipee. Le build passe de 3 minutes a quelques secondes.

Cette technique fonctionne avec tous les gestionnaires de paquets :

dockerfile# Bun
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile

# Python
COPY requirements.txt ./
RUN pip install -r requirements.txt

# Go
COPY go.mod go.sum ./
RUN go mod download

# Rust
COPY Cargo.toml Cargo.lock ./
RUN cargo fetch

Analyser les couches

docker history montre chaque couche avec sa taille :

bashdocker history mon-app
IMAGE          CREATED        CREATED BY                                      SIZE
a1b2c3d4e5f6   2 minutes ago  CMD ["node" "index.js"]                         0B
f6e5d4c3b2a1   2 minutes ago  COPY dir:abc123 in .                            45kB
1a2b3c4d5e6f   2 minutes ago  RUN /bin/sh -c npm ci                           85MB
6f5e4d3c2b1a   2 minutes ago  COPY file:def456 in ./                          120kB
b1a2c3d4e5f6   3 days ago     WORKDIR /app                                    0B
...

85 Mo pour npm ci. Si tu rebuilds et que cette couche vient du cache, tu economises 85 Mo de telechargement et de traitement.

Pour une analyse plus fine, utilise docker image inspect :

bash# Taille totale
docker image inspect mon-app --format '{{.Size}}' | numfmt --to=iec

Ou l'outil dive pour une exploration interactive :

bash# Installation
brew install dive  # Mac
# ou
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive mon-app

dive te montre chaque couche, les fichiers ajoutes, et le gaspillage d'espace.

Le piège de la suppression dans une couche séparée

dockerfileRUN apt-get update && apt-get install -y build-essential
RUN apt-get purge -y build-essential && rm -rf /var/lib/apt/lists/*

Tu penses réduire la taille en supprimant build-essential. Faux. Les fichiers existent dans la couche 1. La couche 2 marque les fichiers comme supprimes, mais ils sont toujours dans l'image. L'image n'est pas plus petite.

La bonne approche :

dockerfileRUN apt-get update \
    && apt-get install -y build-essential \
    && make build \
    && apt-get purge -y build-essential \
    && rm -rf /var/lib/apt/lists/*

Tout dans la meme couche. Les fichiers temporaires ne sont jamais persistes.

Ou mieux : utilise un multi-stage build.

Forcer le rebuild

Parfois, tu veux ignorer le cache :

bash# Rebuild complet sans cache
docker build --no-cache -t mon-app .

# Rebuild a partir d'une etape specifique (BuildKit)
docker build --build-arg CACHEBUST=$(date +%s) -t mon-app .

Avec BuildKit, tu peux aussi invalider une seule couche :

dockerfileARG CACHEBUST=1
RUN echo "bust: $CACHEBUST" && npm ci

Mais en général, si tu as besoin de --no-cache, c'est que tes couches ne sont pas bien ordonnees.

Regles pratiques

  1. Les choses qui changent rarement en haut : FROM, WORKDIR, installation de paquets système
  2. Les dépendances au milieu : COPY package.json + RUN npm ci
  3. Le code source en bas : COPY . .
  4. Nettoyer dans la meme couche : apt-get install && ... && rm -rf
  5. Grouper les commandes RUN quand elles ont un lien logique

Sur paltemps.fr, ces regles sont appliquees sur tous les Dockerfiles. Le gain est mesurable : les rebuilds en CI passent de minutes a secondes quand seul le code change.

Résumé

  • Chaque instruction FROM, RUN, COPY, ADD créé une couche
  • Le cache réutilisé les couches inchangees, mais invalide tout ce qui suit une couche modifiee
  • Copie les fichiers de dépendances avant le code source pour exploiter le cache
  • Les suppressions dans une couche séparée ne reduisent pas la taille de l'image
  • docker history et dive permettent d'analyser les couches

Precedent : 04 - Dockerfile | Suivant : 06 - .dockerignore

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.