2.2Optimiser le build : couches, cache et multi-stage
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.
Le saviez-vous ?
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
- Buildez votre app « naïve » (tout d'un bloc) et notez taille + durée.
- Réordonnez : dépendances d'abord, code ensuite ; rebuildez après un changement de code.
- Convertissez en multi-stage (build outillé, runtime slim) et re-mesurez.
- Comparez history avant/après et gardez la recette qui gagne.
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.
- 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.
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é.
Pour un build rapide, où placer COPY . . (le code) ?