infra / docker
Multi-stage builds
Explication
Ce que vous allez apprendre
- Comprendre pourquoi les outils de build ne devraient jamais atterrir dans l'image de production
- Découper un Dockerfile en plusieurs stages avec
FROM ... AS <nom> - Copier uniquement le résultat nécessaire d'un stage à l'autre avec
COPY --from - Utiliser une image
scratchpour un binaire compilé (Go) et obtenir une image quasi vide - Cibler un stage précis au build (
--target) pour les tests ou le débogage, sans publier l'image finale
Dans quel contexte ?
Une équipe backend en Node.js/TypeScript construit son image avec un seul stage : elle installe toutes les dépendances (y compris celles de développement), compile le TypeScript, puis lance l'application — résultat, une image de 1,2 Go alors que le code compilé ne pèse que quelques mégaoctets. En passant à un multi-stage build, un premier stage compile le code avec tous les outils nécessaires, et le stage final ne récupère que le dossier dist/ compilé : l'image finale tombe à 180 Mo, sans compilateur ni code source exposé.
Le problème : l'image finale hérite de tout
Compiler ou construire une application nécessite souvent des outils lourds — compilateurs, dépendances de développement, code source complet — qui n'ont strictement aucune utilité une fois l'application prête à tourner. Sans précaution, un Dockerfile classique embarque pourtant tout cela dans l'image finale, ce qui l'alourdit inutilement et augmente sa surface d'attaque : plus il y a d'outils installés, plus il y a de failles potentielles exploitables si l'application est compromise.
| Stage | Contenu | Présent dans l'image finale ? |
|---|---|---|
builder | Compilateur, sources, dépendances de dev | Non |
production | Uniquement le résultat compilé | Oui |
Astuce
Utilise --target pour construire uniquement jusqu'à un stage précis (docker build --target builder -t debug .) : c'est très utile pour déboguer une étape intermédiaire du build sans attendre que tout le Dockerfile se termine.
L'idée : séparer construction et exécution
Un multi-stage build consiste à découper le Dockerfile en plusieurs étapes successives (des "stages"), chacune démarrant sa propre image de base avec FROM ... AS <nom>. Un premier stage, souvent appelé "builder", contient tous les outils lourds nécessaires pour compiler ou préparer l'application. Un stage final, beaucoup plus léger, ne récupère du premier stage QUE le résultat fini (un binaire compilé, du code transpilé), grâce à COPY --from=<stage>. Tout le reste — compilateur, sources, dépendances de développement — reste dans le stage builder et n'apparaît jamais dans l'image livrée.
Un exemple qui parle : Go et "scratch"
Le cas le plus spectaculaire est celui d'un langage compilé comme Go : le binaire final n'a besoin d'aucun runtime pour s'exécuter, il peut donc atterrir dans une image scratch, littéralement vide — pas même un shell. L'image finale ne contient alors que l'application elle-même, ce qui la rend minuscule et extrêmement difficile à exploiter en cas de faille, faute d'outils disponibles à l'intérieur pour un attaquant.
Lien avec la leçon suivante
Cette technique est l'un des leviers les plus efficaces de l'optimisation d'images (couverte juste après), bien plus impactant que de simplement choisir une image de base légère.
Commandes & code
Multi-stage builds
# Problème : les outils de build (compilateurs, dépendances de dev) n'ont rien à faire
# dans l'image finale de production -> ils l'alourdissent et augmentent la surface d'attaque.
# --- Exemple Node.js : stage de build séparé du stage d'exécution ---
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci # installe TOUTES les dépendances, y compris devDependencies
COPY . .
RUN npm run build # compile TypeScript -> JavaScript dans /app/dist
FROM node:20-slim AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # uniquement les dépendances de production
COPY --from=builder /app/dist ./dist # copie SEULEMENT le résultat compilé depuis le stage builder
CMD ["node", "dist/main.js"]
# L'image finale ne contient ni le code source TypeScript, ni le compilateur, ni les devDependencies# --- Exemple Go : le cas le plus spectaculaire, binaire statique dans une image quasi vide ---
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/serveur ./cmd/serveur
FROM scratch
# "scratch" = image totalement vide, pas même un shell -> la plus petite image possible
COPY --from=builder /app/serveur /serveur
ENTRYPOINT ["/serveur"]# --- Réutiliser un stage intermédiaire pour les tests, sans le publier ---
FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
FROM base AS test
COPY requirements-dev.txt .
RUN pip install --no-cache-dir -r requirements-dev.txt
COPY . .
RUN pytest
FROM base AS production
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0"]# Construire un stage précis (utile en CI pour lancer les tests avant de builder la prod)
docker build --target test -t mon-api:test .
docker build --target production -t mon-api:prod .
# Construire uniquement jusqu'à un stage, pour déboguer une étape intermédiaire
docker build --target builder -t mon-api:debug-builder .
docker run -it mon-api:debug-builder bashRésumé
- Chaque
FROM ... AS <nom>démarre un nouveau stage ;COPY --from=<stage>transfère uniquement ce qui est nécessaire. - Les outils de build (compilateurs, devDependencies) restent dans le stage builder, absents de l'image finale.
--targetpermet de construire un stage précis (tests, debug) sans publier l'image de production.
Exercices pratiques
Mission : réduire drastiquement une image Node.js de 1,2 Go
Objectif : Convertir un Dockerfile mono-stage en multi-stage build et exploiter --target pour déboguer une étape intermédiaire.
Contexte
L'image mon-api (Node.js/TypeScript) pèse 1,2 Go car elle installe toutes les dépendances (dev incluses) et garde le code source dans l'image finale. Tu dois la restructurer en plusieurs stages, tout en gardant la possibilité de déboguer l'étape de compilation isolément.