2.2 · Optimiser le build : couches, cache et multi-stage

Niveau 2 · Dockerfile & build

2.2Optimiser le build : couches, cache et multi-stage

Objectif : Diviser par 10 le temps de build et la taille des images grâce au cache et au multi-stage.
Temps estimé : 14 min · lecture autonome · Si vous n'avez que 5 min : lisez « L'essentiel en 30s » + le schéma.

L'essentiel en 30 secondes

  • L'ordre pilote le cache : Docker rejoue les couches jusqu'à la première modifiée — stable d'abord, changeant ensuite.
  • Multi-stage = construire gros, livrer maigre : on compile dans un stage outillé, on ne livre que le runnable — COPY --from=build.
  • Mesurer avant d'optimiser : docker images et docker history prouvent le gain — viser sous 200 Mo pour une app web.

Mini-cas filé — Idir et le build qui réinstallait tout

L'app emballée d'Idir (2.1) fonctionne, mais chaque virgule modifiée relance npm install et le build dure des minutes ; l'image dépasse le gigaoctet. Idir réordonne : dépendances stables d'abord (package*.json + install), code changeant ensuite — un changement de code ne rejoue plus l'installation. Puis il passe en multi-stage : un stage outillé compile, un stage slim ne reçoit que le résultat via COPY --from=build. Verdict chiffré : de 1 Go à environ 150 Mo. En 2.3, Idir taguera cette image maigre ; en 2.5, il la durcira.

Suite du fil rouge 2.1 : l'app d'Idir devient rapide à builder et maigre à livrer — et continue en 2.3, 2.4 et 2.5.

1. Le cache : l'ordre change tout

Docker rejoue les couches dans l'ordre et réutilise le cache jusqu'à la première couche modifiée : tout ce qui suit est rejoué. Donc on place d'abord le stable (les dépendances : COPY package*.json puis install) et ensuite le changeant (le code : COPY . .). Inverser l'ordre re-télécharge tout à chaque virgule modifiée. Idir copie les manifests et installe AVANT le code : un changement de code ne rejoue que les dernières couches.

2. Le multi-stage : construire gros, livrer maigre

On compile dans une image outillée (stage « build » avec SDK complet) puis on copie le résultat seul dans une image d'exécution légère (COPY --from=build). Le SDK, les sources et les caches restent sur le quai : l'image finale ne contient que le nécessaire pour tourner. Chaque FROM ouvre un stage ; on ne livre que le dernier. Idir construit en image complète et livre en node:20-slim.

3. Mesurer avant d'optimiser

docker images donne la taille, docker history le poids par couche : on optimise ce qu'on mesure. Objectif réaliste : une app web sous les 200 Mo (base slim + multi-stage). Un .dockerignore complet (node_modules, .git, dist) allège le contexte envoyé au daemon. En dessous du seuil, on chasse les dépendances inutiles, pas les octets. Idir note taille et durée avant/après : chiffres à l'appui, pas d'intuition.

Erreur d'interprétation fréquente : « COPY . . en première ligne, c'est plus simple et ça ne change rien ». Faux : la moindre modification invalide le cache dès la première couche et rejoue tout, installation comprise. La règle d'Idir : dépendances stables d'abord (cachées), code changeant ensuite — et on ne livre que le runnable en multi-stage.
Un build rapide et maigre Dépendances d'abord (cache), code ensuite, livraison maigre Dépendances (cache) Stabled'abord, install cachéCode (changeant) Modifiésouvent, rejoué seulRuntime maigre Multi-stage,slim, mesuré
Dépendances d'abord (cache), code ensuite, livraison maigre
Version texte du schéma (SEO / lecteurs d'écran / traduction) : 1) Idir ORDONNE : manifests et installation d'abord (couches cachées), code ensuite — un changement de code ne rejoue plus npm install. 2) Il SÉPARE : stage build outillé pour compiler, stage slim qui ne reçoit que le résultat via COPY --from=build. 3) Il MESURE : docker images et docker history avant/après, contexte allégé par .dockerignore. Leçon : l'ordre fait la vitesse, le multi-stage fait la taille, la mesure prouve les deux.

Le saviez-vous ?

Idir a déjà vu un build réinstaller 400 Mo de dépendances pour une virgule déplacée dans un commentaire — COPY . . en première ligne.
Vrai : une ligne mal placée peut coûter des minutes à chaque build.

L'ordre des lignes, c'est du temps de vie.

🔎 Exemple concret — 🔎 Exemple concret — Node : de 1 Go à 150 Mo

Le Dockerfile d'Idir, version optimisée et mesurée :

# stage 1 : construction
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# stage 2 : exécution maigre
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "dist/server.js"]

Lecture : le SDK et les sources restent dans le stage build, abandonné après ; le stage final ne contient que le runnable.

Leçon : docker history prouve quelles couches ont fondu — mesurer avant d'applaudir.

Vocabulaire de la section

Cache de build
La réutilisation des couches inchangées entre deux builds : tout est rejoué à partir de la première couche modifiée.

Exemple simple : Idir ne rejoue plus npm install quand seul son code change.

Ce que ce n'est pas : une image finale plus petite.

Réflexe : stable d'abord pour cacher.

Multi-stage
Plusieurs instructions FROM : on construit dans un stage outillé et on ne livre que le dernier stage, maigre.

Exemple simple : Idir compile en node:20 puis livre en node:20-slim.

Ce que ce n'est pas : un simple réordonnancement des couches.

Réflexe : construire gros, livrer maigre.

COPY --from
Copier depuis un stage précédent (pas depuis le contexte) : on ne rapatrie que le résultat compilé.

Exemple simple : Idir copie /app/dist depuis le stage build vers le stage slim.

Ce que ce n'est pas : un COPY classique depuis le contexte.

Réflexe : ne livrer que le runnable.

history
Le détail couche par couche d'une image (docker history) : poids et origine de chaque couche.

Exemple simple : Idir compare history avant/après pour prouver le gain.

Ce que ce n'est pas : la taille totale seule (docker images).

Réflexe : mesurer par couche avant d'optimiser.

En pratique — En pratique — Fondre une image

  1. Buildez votre app « naïve » (tout d'un bloc) et notez taille + durée.
  2. Réordonnez : dépendances d'abord, code ensuite ; rebuildez après un changement de code.
  3. Convertissez en multi-stage (build outillé, runtime slim) et re-mesurez.
  4. Comparez history avant/après et gardez la recette qui gagne.
Vos builds sont rapides et vos images maigres, chiffres à l'appui.

Script prêt à dire — justifier l'optimisation (30 s)

« Avant, chaque modification relançait toute l'installation et l'image dépassait le gigaoctet. Maintenant les dépendances sont cachées, seul le code est rejoué, et le multi-stage ne livre que le runnable : environ 150 Mo. La preuve : docker images et history avant/après. Plus vite à builder, plus léger à déployer. »

À dire en montrant les deux mesures d'Idir, chiffres à l'appui.

Modèle copier-coller — Dockerfile multi-stage Node

FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-slim
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY package*.json ./
RUN npm ci --omit=dev
EXPOSE 3000
CMD ["node", "dist/server.js"]

Cache mort

COPY . . en première ligne : chaque build réinstalle tout — Idir remonte les dépendances avant le code.

SDK livré

Image de prod avec le SDK complet : Idir bascule en multi-stage et ne livre que le dist.

Contexte lourd

Contexte de 800 Mo : Idir écrit un .dockerignore (node_modules, .git, dist) et le build respire.

Optimiser à l'aveugle

« C'est plus léger, je le sens » : Idir sort docker history et prouve le gain couche par couche.

✓ Bonnes pratiques

  • Dépendances d'abord, code ensuite, toujours.
  • Un .dockerignore complet (node_modules, .git, dist).
  • Livrer en slim, construire en complète.

✗ Pièges fréquents

  • COPY . . en première ligne (cache mort).
  • Livrer le SDK en production.
  • Optimiser sans mesurer (history/images).

Points clés à retenir

  • L'ordre des couches pilote le cache (stable d'abord, changeant ensuite).
  • Multi-stage : on ne livre que le runnable (COPY --from=build).
  • On mesure : docker images (taille) + docker history (par couche).
  • .dockerignore complet : le contexte ne doit pas transporter l'inutile.

Défi 48h — appliquez demain

Sur votre app emballée (2.1) : notez taille + durée, réordonnez dépendances puis code, convertissez en multi-stage, puis comparez history avant/après.

Checklist sortie de section
  • Je peux ordonner un Dockerfile pour le cache (stable puis changeant).
  • Je sais écrire un build multi-stage qui ne livre que le runnable.
  • Je mesure avec images + history avant d'annoncer un gain.
Auto-évaluation 1-5 : est-ce que mon COPY . . vient après l'install ?est-ce que mon image finale contient autre chose que le runnable ?

Questions fréquentes

Pourquoi mon build rejoue npm install à chaque fois ?

Parce que COPY . . précède RUN npm ci : le moindre fichier modifié invalide le cache dès cette couche. Copiez package*.json et installez AVANT le code : seul un changement de dépendances rejouera l'installation.

Le multi-stage ralentit-il le build ?

Un peu (deux stages à construire), mais il divise la taille livrée et la surface d'attaque. En CI avec cache, le surcoût est marginal face au gain au déploiement et à la sécurité.

Vérifiez votre compréhension

Pour un build rapide, où placer COPY . . (le code) ?

Ressources supplémentaires (optionnel, pour aller plus loin)

Examen du niveau 2Questions inédites et durcies pour valider vos acquis