Build multi-étapes Docker en pratique : passer d'une image de prod de 1 Go à 10 Mo

« Le push de l’image a échoué, timeout. »
C’était un vendredi après-midi l’année dernière, et le pipeline CI/CD était tout rouge. Je fixais une image Go de 980 Mo et j’ai senti le coup au ventre. Un collègue ops s’est approché en soupirant : « Ton image est plus grosse que le film que j’ai téléchargé à midi. »
Ensuite j’ai adopté le build multi-étapes.
10 Mo. Même application, mêmes fonctionnalités : de 980 Mo à 10 Mo. 99 % du volume disparu ; le push CI/CD est passé de 3 minutes à 3 secondes.
Dans cet article, je partage des techniques concrètes de build multi-étapes : modèles Dockerfile complets pour Go, Node.js et Python, plus 5 erreurs fréquentes que j’ai apprises à mes dépens. Si vous voulez passer d’une image de prod « obèse » à « maigre », continuez la lecture.
Pourquoi votre image est-elle si volumineuse ?
Franchement, la plupart des images Docker gonflées ont les mêmes causes.
J’avais écrit un Dockerfile comme celui-ci :
FROM ubuntu:20.04
RUN apt-get update && apt-get install -y golang
COPY . /app
WORKDIR /app
RUN go build -o myapp
CMD ["./myapp"]
Ça a l’air correct, non ? Puis docker images — 980 Mo.
Le problème en quatre mots : on garde trop, on jette pas assez.
Concrètement :
- Image de base trop lourde :
ubuntu:20.04fait déjà 77 Mo ; avec la chaîne d’outils Go, on dépasse 900 Mo - Outils de compilation qui restent : gcc, make, git — inutiles en production
- Cache non nettoyé : les caches apt/apk restent dans les couches
- Dépendances en trop : devDependencies et frameworks de test embarqués aussi
Image : vous partez en voyage avec valise, sac de couchage, tente et réchaud… pour dormir à l’hôtel. Le build multi-étapes, c’est n’emporter que l’essentiel — vêtements et trousse de toilette — le reste reste à la maison.
Selon la documentation Docker, une app Go typique non optimisée fait environ 800 Mo–1 Go ; optimisée, 10–20 Mo. L’écart est brutal.
Principe du build multi-étapes
L’idée est simple : séparer l’environnement de build et celui d’exécution.
Un Dockerfile classique mélange compilation, packaging et run dans une seule image. Le multi-étapes autorise plusieurs instructions FROM, chacune ouvrant une nouvelle phase.
Exemple minimal :
# Phase 1 : build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# Phase 2 : exécution
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]
Deux syntaxes clés :
FROM ... AS builder: nommer la phaseCOPY --from=builder: copier depuis cette phase
Docker exécute chaque phase dans l’ordre, mais l’image finale ne contient que la dernière. Outils de compilation, caches de dépendances — tout est abandonné.
Selon le tutoriel iximiuz Labs (2026), le multi-étapes exploite le mécanisme de couches : chaque FROM démarre un contexte indépendant ; vous copiez d’une phase à l’autre, sans que le superflu n’entre dans l’image finale.
Comme rénover une maison : phase 1, l’équipe avec perceuse et scie ; phase 2, vous emménagez avec meubles et électroménager. L’équipe part, les outils aussi — il ne reste que ce dont vous avez besoin.
Cas pratiques : modèles pour trois langages
Go : de 980 Mo à 10 Mo
Go est idéal pour le multi-étapes : binaire statique possible.
Dockerfile complet :
# Phase de build
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Copier d'abord go.mod et go.sum pour le cache
COPY go.mod go.sum ./
RUN go mod download
# Puis le code source et la compilation
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-extldflags "-static"' -o myapp .
# Phase d'exécution
FROM scratch
COPY --from=builder /app/myapp /myapp
# Certificats CA si appels HTTPS
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/myapp"]
Astuces :
FROM scratch: image vide, seulement votre binaireCGO_ENABLED=0: binaire statique pur- Certificats CA : obligatoires pour HTTPS
- Cache des deps : copier go.mod/go.sum avant
go mod downloadpour ne pas retélécharger à chaque changement de code
Résultat : environ 10 Mo. Contre 980 Mo, −99 %.
Si scratch est trop strict (pas de shell), utilisez alpine :
FROM alpine:3.18
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
Environ 15 Mo, avec un shell pour docker exec.
Node.js : de 900 Mo à 120 Mo
Un peu plus délicat à cause de node_modules.
Dockerfile complet :
# Phase de build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Phase de production
FROM node:18-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/index.js"]
Points :
npm ci --only=production: seulementdependencies, pasdevDependenciesnpm cache clean --force: sinon le cache reste dans une couche- Build séparé : compilation TypeScript en builder, JS seul en prod
Données Oak Oliver Engineering : Express typique ~900 Mo → ~120 Mo (−87 %).
Python : de 300 Mo à 100 Mo
Pas d’étape de compilation, mais des paquets lourds (numpy, pandas…).
Dockerfile complet :
# Phase de build
FROM python:3.9-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Phase de production
FROM python:3.9-alpine
WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]
pip install --user installe dans /root/.local, puis on copie tout le dossier.
Astuces :
--no-cache-dir: évite le cache pip dans l’image- slim vs alpine : slim en build (compatibilité), alpine en prod (taille)
- venv : pour des deps complexes, venv peut valoir mieux que
--user
Projet FastAPI + SQLAlchemy testé : ~300 Mo → ~100 Mo.
Choix de l’image de base : Alpine vs Distroless vs Slim
La phase d’exécution demande un compromis.
| Caractéristique | Alpine | Distroless | Slim |
|---|---|---|---|
| Taille de base | 3-5 Mo | 20-65 Mo | 50-100 Mo |
| Sécurité | Moyenne | Très élevée | Moyenne |
| Débogage | Facile (shell) | Difficile (pas de shell) | Facile (shell) |
| Compatibilité | Pièges (glibc) | Bonne | Bonne |
| Cas d’usage | Binaire Go statique | Exigences sécurité élevées | Node.js/Python |
Alpine : le plus petit, attention à glibc
Alpine utilise musl libc, pas glibc. OK pour Go statique ; parfois problématique pour certaines deps Python/Node.
J’ai eu ce cas : numpy qui plante sur Alpine avec ImportError: cannot import name 'random'. Cause : incompatibilité musl/glibc.
Solutions :
- installer
libc6-compat:apk add libc6-compat - ou passer à
slimau lieu d’alpine
Distroless : référence sécurité, débogage difficile
Série Google : pas de shell, pas de gestionnaire de paquets, le strict minimum.
Selon danieldemmel.me, Distroless réduit beaucoup de CVE critiques — pas de shell pour exécuter des commandes.
Contrepartie : pas de docker exec pour inspecter. Il faut s’appuyer sur logs et monitoring.
Pour une sécurité maximale :
FROM gcr.io/distroless/static-debian11
COPY --from=builder /app/myapp /
ENTRYPOINT ["/myapp"]
Slim : compromis
Images officielles -slim (node:18-slim, python:3.9-slim) : entre Alpine et l’image complète.
Un peu plus lourdes qu’Alpine, meilleure compatibilité, shell pour déboguer. Si vous ne voulez pas vous battre avec musl/glibc, slim est le choix tranquille.
Mes recommandations :
- Go :
scratchoualpineen priorité - Node.js/Python :
slimd’abord, puisalpinesi tout passe - Haute sécurité :
distroless, avec plan de logs/monitoring
Pièges : 5 erreurs fréquentes et solutions
Après tant de Dockerfiles, voici les 5 erreurs les plus courantes.
Erreur 1 : COPY —from=0 en copie totale
Les débutants copient toute la phase précédente :
# Mauvais exemple
FROM builder
COPY --from=0 /app /app
Tout part : chaîne Go, cache npm, fichiers temporaires — l’image regonfle.
Bonne pratique : copier uniquement le nécessaire.
# Bon exemple
COPY --from=builder /app/myapp /myapp
COPY --from=builder /app/dist /dist
Erreur 2 : cache non nettoyé
Le cache apt/apk reste dans la couche même après suppression.
# Mauvais (le cache reste dans la couche précédente)
RUN apt-get update && apt-get install -y curl
RUN apt-get clean
Nettoyage dans la même couche :
# Bon
RUN apt-get update && apt-get install -y curl && apt-get clean && rm -rf /var/lib/apt/lists/*
Ou avec --no-cache :
RUN apk add --no-cache curl
Erreur 3 : compatibilité glibc sur Alpine
musl vs glibc — certaines deps Python/Node cassent.
Erreur typique :
ImportError: cannot import name 'random' from 'numpy.random'
Solution : libc6-compat ou image slim.
Erreur 4 : pas d’utilisateur non-root
Par défaut le conteneur tourne en root — risque accru.
RUN adduser -D appuser
USER appuser
En cas d’intrusion, privilèges limités.
Erreur 5 : oublier .dockerignore
.dockerignore est la liste d’exclusion du Dockerfile. Sans lui, COPY . . embarque .git, node_modules, tests…
Exemple .dockerignore :
.git
.gitignore
node_modules
npm-debug.log
Dockerfile
.dockerignore
*.md
.env
Contexte de build plus léger, build plus rapide.
Conclusion
Le build multi-étapes est la technique la plus utile pour alléger une image Docker.
En une phrase : les outils de build restent en build, l’image de run ne garde que l’app.
Récap des chiffres :
- Go : 980 Mo → 10 Mo (−99 %)
- Node.js : 900 Mo → 120 Mo (−87 %)
- Python : 300 Mo → 100 Mo (−67 %)
Si vous n’avez pas encore testé, prenez un projet, adaptez un modèle ci-dessus, comparez avec docker images.
Vous devriez être agréablement surpris — au minimum, le push CI/CD ne timeoutera plus.
Optimisation d'image Docker par build multi-étapes
Processus complet pour réduire une image Docker au minimum
⏱️ Estimated time: 30 min
- 1
Step 1: Analyser la composition de l'image actuelle
Utilisez `docker history` pour voir la taille de chaque couche :
```bash
docker history your-image:tag
```
Identifiez les couches les plus volumineuses, en général :
• l'image de base elle-même
• les outils de build et dépendances de compilation
• le cache du gestionnaire de paquets - 2
Step 2: Rédiger un Dockerfile multi-étapes
Créez un Dockerfile avec une phase de build et une phase d'exécution :
```dockerfile
# Phase de build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# Phase d'exécution
FROM alpine:3.18
COPY --from=builder /app/myapp /myapp
ENTRYPOINT ["/myapp"]
```
Points clés :
• nommer les phases avec AS
• COPY --from=builder ne copie que le nécessaire - 3
Step 3: Construire et comparer la taille des images
Construisez la nouvelle image et comparez le volume :
```bash
docker build -t myapp:optimized .
docker images | grep myapp
```
Comparez la différence avant/après optimisation. - 4
Step 4: Vérifier que l'application fonctionne
Lancez le conteneur et testez l'application :
```bash
docker run -d -p 8080:8080 myapp:optimized
curl http://localhost:8080/health
```
Vérifiez que tout fonctionne sans dépendance manquante. - 5
Step 5: Déployer en production
Mettez à jour le pipeline CI/CD pour utiliser la nouvelle image :
• pousser vers le registre d'images
• mettre à jour le Deployment Kubernetes ou docker-compose.yml
• valider le déploiement
FAQ
Le build multi-étapes ralentit-il la construction ?
Comment choisir entre Alpine et Distroless ?
Quels langages en bénéficient le plus ?
Comment gérer les fichiers de configuration en multi-étapes ?
De combien peut-on réduire le volume d'image ?
Quelles précautions avec FROM scratch ?
7 min de lecture · Publié le: 19 avr. 2026 · Mis à jour le: 27 juil. 2026
Guide pratique Docker
Si vous arrivez depuis la recherche, le plus rapide est de passer à l’article précédent ou suivant de cette série.
Précédent
Build multi-étapes Docker : réduire les images Go/Java/Rust de GB à MB
Analyse approfondie du build multi-étapes Docker, avec cas réels montrant comment optimiser une image Go de 98 %, Java de 86 % et Rust de 99,4 %. Dockerfile complets et retours d'expérience.
Partie 5 sur 38
Suivant
Accélérer les builds Docker : guide pratique du cache pour des builds 10× plus rapides
Maîtrisez le cache par couches Docker, la configuration .dockerignore et l'optimisation du Dockerfile pour passer de 10 minutes à 30 secondes. Exemples complets et montage de cache BuildKit.
Partie 7 sur 38



Commentaires
Connectez-vous avec GitHub pour laisser un commentaire