Multi-stage builds : images de production
Construire gros, livrer maigre : builder vs runner.
Objectifs
À la fin de cette leçon, vous saurez :
- écrire un Dockerfile multi-stage pour une application TypeScript ;
- comprendre ce que chaque stage apporte et abandonne ;
- mesurer et comparer la taille des images.
🔗 Pour vous rafraîchir la mémoire : commandes docker de base · package.json et scripts npm
Le problème du build unique
Votre image Node de développement contient : les sources TS, TypeScript lui-même, tsx, jest, eslint, node_modules complet... Des centaines de Mo qui ne servent À RIEN à l'exécution — et élargissent la surface d'attaque (les outils de build sont des cibles).
La solution : plusieurs stages
# ---- STAGE 1 : builder ----
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # produit dist/ + node_modules prod seulement
RUN npm prune --omit=dev # retire jest, typescript, eslint...
# ---- STAGE 2 : runner ----
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER node
CMD ["node", "dist/main.js"]
docker build -t todo-api:prod .
docker images todo-api # comparez avec la version single-stage
Ce que chaque stage fait
| Stage | Contient | Devient |
|---|---|---|
| builder | sources, outils, devDependencies | jetable après copie |
| runner | dist/ + deps prod uniquement | l'image publiée |
Le COPY --from=builder ne transfère que les dossiers nommés : tout le reste (sources TS, cache npm, outils) disparaît. Résultat type : 1.2 Go → 180 Mo.
Bénéfices au-delà de la taille :
- pas de TypeScript/jest dans l'image produite = rien à exploiter ;
NODE_ENV=productionexplicite ;- le stage builder peut utiliser une autre base que le runner (compilateurs natifs sans les embarquer).
Variantes utiles
Tests dans un stage dédié (jamais dans l'image finale) :
FROM node:22-alpine AS test
WORKDIR /app
COPY --from=builder /app ./
CMD ["npm", "test"] # ciblé par docker build --target test
La CI construit --target test pour exécuter la suite ; l'image publiée part du target final.
Cache des layers inchangés : gardez COPY package.json → npm ci AVANT COPY . . même en multi-stage — le principe reste identique.
Vérifier ce qu'une image contient
docker run --rm todo-api:prod ls -la /app # pas de src/, pas de tests
docker history todo-api:prod # poids de chaque layer
history révèle aussi les secrets malheureusement copiés dans un layer antérieur : vérification de sécurité gratuite avant publication.
Exercice
- Convertissez votre Dockerfile en multi-stage ; comparez les tailles.
- Vérifiez qu'il n'y a ni sources TS ni devDependencies dans l'image finale.
- Ajoutez le stage test et exécutez-le via --target.
- Pourquoi USER node reste-t-il indispensable dans le stage final ?
Résumé
- Multi-stage = construire gros, livrer maigre.
- COPY --from = seule frontière entre stages.
- history + ls dans l'image = audit de contenu avant push.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 4. Le stage builder a besoin de droits étendus pour compiler ; le runner n'exécute qu'un fichier JS. root dans le runner signifie : compromission applicative = compromission du conteneur entier. Avec USER node, l'attaquant est enfermé dans un compte sans droits — défense en profondeur du chapitre sécurité appliquée aux conteneurs.