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.jsonavantCOPY . - Comment analyser les couches avec
docker history - Comment réduire la taille de tes images en comprenant les couches
Prerequisites
- Avoir lu l'article 04 - Dockerfile
- Un Dockerfile fonctionnel pour experimenter
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 :
- L'instruction est-elle identique a la dernière fois ?
- Pour
COPY/ADD: les fichiers copies ont-ils change (checksum) ? - 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 utiliseRUN npm ci: instruction identique, couche précédente en cache, cache utiliseCOPY . .: 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
- Les choses qui changent rarement en haut :
FROM,WORKDIR, installation de paquets système - Les dépendances au milieu :
COPY package.json+RUN npm ci - Le code source en bas :
COPY . . - Nettoyer dans la meme couche :
apt-get install && ... && rm -rf - 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,ADDcréé 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 historyetdivepermettent d'analyser les couches
Precedent : 04 - Dockerfile | Suivant : 06 - .dockerignore