Tu sais écrire un Dockerfile basique — FROM, COPY, RUN, CMD. Ça tourne. Mais ton image pèse 800 Mo, chaque build prend 4 minutes, et ton .env s’est retrouvé dans le registry. Bienvenue dans le chapitre qui transforme un Dockerfile fonctionnel en un Dockerfile production-ready.
On va voir comment séparer le build du runtime avec les multi-stage builds, exploiter le cache comme un levier de productivité, et adopter les réflexes qui font la différence entre une image amateur et une image professionnelle.
Pourquoi c’est important
Une image Docker mal optimisée, c’est une bombe à retardement. Plus elle est grosse, plus le pull est lent, plus le déploiement traîne, plus la surface d’attaque est large. En production, chaque Mo compte — surtout quand tu déploies 50 fois par jour sur un cluster Kubernetes.
🔥 Cas réel : Une équipe déployait une image Node.js de 1.2 Go. Le pull sur chaque nœud prenait 45 secondes. En passant aux multi-stage builds et à Alpine, l’image est tombée à 90 Mo — le déploiement est passé de 2 minutes à 15 secondes. Multiplié par 200 déploiements par semaine, ça représente des heures récupérées.
Les Dockerfiles optimisés, c’est aussi une question de sécurité. Moins de paquets dans l’image finale = moins de CVE potentielles. Une image distroless n’a même pas de shell — un attaquant qui pénètre le conteneur ne peut rien exécuter.
Comprendre le multi-stage build
Le principe est simple : tu utilises plusieurs instructions FROM dans un seul Dockerfile. Chaque FROM démarre un nouveau stage. Tu copies ce dont tu as besoin d’un stage à l’autre avec COPY --from=, et seul le dernier stage produit l’image finale.
Voici un exemple complet avec une application Go. Le premier stage compile le binaire dans un environnement complet (SDK Go, dépendances). Le second stage ne contient que le binaire sur une base minimale :
# Stage 1 : build avec le SDK Go complet (~300 Mo)
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Stage 2 : image finale ultra-légère (~8 Mo)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/server"]
💡 Tip DevOps : Le flag -ldflags="-s -w" supprime les symboles de debug du binaire Go — ça réduit sa taille de 30% sans affecter le fonctionnement.
Le même principe s’applique en Node.js. Tu sépares l’installation des dépendances, le build (TypeScript, bundler), et le runtime en trois stages distincts. Les devDependencies, le code source TypeScript et les outils de build restent dans les stages intermédiaires — l’image finale ne contient que le JavaScript compilé et les dépendances de production.
🧠 À retenir : Pense aux multi-stage builds comme une chaîne de montage. Chaque poste fait son travail, et seul le produit fini sort de l’usine.
Commandes et techniques essentielles
Exploiter le cache de layers
Docker met en cache chaque instruction. Quand une ligne change, toutes les instructions suivantes perdent leur cache. L’ordre de tes instructions est donc critique.
Compare ces deux approches. La première invalide le cache des dépendances à chaque changement de code. La seconde ne réinstalle les dépendances que si package.json change :
# ❌ Mauvais : tout changement de code → réinstallation complète
COPY . .
RUN npm ci
# ✅ Bon : dépendances cachées séparément du code
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
⚠️ Attention : Cette règle s’applique à tous les langages. En Python, copie requirements.txt avant le code. En Go, copie go.mod et go.sum en premier. C’est le pattern d’optimisation numéro un.
Le .dockerignore
Sans .dockerignore, Docker envoie tout le répertoire au daemon — y compris node_modules, .git, et potentiellement tes fichiers .env. Voici un fichier type pour un projet Node.js :
node_modules
.git
.env
*.md
dist
coverage
Dockerfile
docker-compose*.yml
Regrouper les layers
Chaque RUN crée un layer. Et un layer ne peut pas annuler l’espace pris par un layer précédent. L’installation et le nettoyage doivent être dans la même instruction :
# ❌ Le rm ne réduit rien — le layer précédent contient déjà les fichiers
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ Un seul layer, nettoyage effectif
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Choisir la bonne base
- Alpine (~5 Mo) : légère, mais utilise
muslau lieu deglibc— teste la compatibilité - Slim (~60 Mo) : bon compromis, basée sur Debian mais allégée
- Distroless (~2-20 Mo) : pas de shell, pas de package manager — idéal pour la prod
- Scratch (0 Mo) : image vide, uniquement pour les binaires statiques
💡 Tip DevOps : Utilise docker history <image> pour voir la taille de chaque layer, et l’outil open-source dive pour une analyse visuelle détaillée.
Cas concret entreprise
Tu travailles sur une API e-commerce en Node.js/TypeScript. L’équipe a 12 développeurs, le CI/CD tourne 80 fois par jour. Voici le Dockerfile de production :
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production && npm cache clean --force
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src/ ./src/
RUN npm run build
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system app && adduser --system --ingroup app app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
USER app
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
🔥 Cas réel : Avec ce Dockerfile, l’image passe de 450 Mo (build naïf) à 120 Mo. Le build incrémental (changement de code uniquement) prend 8 secondes au lieu de 2 minutes grâce au cache des dépendances. Le HEALTHCHECK intégré permet à Docker Swarm ou Kubernetes de redémarrer automatiquement un conteneur planté.
Points clés de ce Dockerfile :
- Trois stages : dépendances prod, build TypeScript, image finale
- Utilisateur non-root (
app) — jamais de process tournant en root - Forme exec pour CMD — le process reçoit directement les signaux POSIX
- HEALTHCHECK intégré — monitoring sans outil externe
Pièges fréquents
1. FROM python:latest — Le tag latest change sans prévenir. Un build qui marchait lundi peut casser mercredi. Utilise toujours un tag précis : python:3.12-slim.
2. Secrets dans l’image — Jamais de ENV DB_PASSWORD=xxx ou COPY .env .. Les variables d’environnement gravées dans l’image sont visibles avec un simple docker inspect. Passe les secrets au runtime via -e ou Docker Secrets.
⚠️ Attention : Même si tu supprimes un fichier secret dans un layer ultérieur, il reste accessible dans les layers précédents. Un docker save + extraction du tar suffit pour le retrouver.
3. apt-get upgrade dans le Dockerfile — L’image de base est ton contrat de version. Si tu as besoin de patchs de sécurité, mets à jour le tag de l’image de base.
4. Outils de debug en production — vim, curl, htop élargissent la surface d’attaque. Utilise les multi-stage builds pour les confiner au stage de build.
5. ADD au lieu de COPY — ADD fait de l’extraction automatique d’archives et du téléchargement d’URLs. C’est un comportement implicite dangereux. Préfère COPY pour les fichiers locaux.
6. Pas de WORKDIR — Sans WORKDIR, tes fichiers atterrissent à la racine du filesystem. C’est sale et ça peut créer des conflits.
Exercice
Voici un Dockerfile volontairement truffé d’erreurs. Identifie au moins 6 problèmes, puis réécris-le proprement en appliquant tout ce qu’on a vu :
FROM ubuntu:latest
RUN apt-get update
RUN apt-get install -y python3 python3-pip
RUN pip3 install flask redis
COPY . .
ADD config.tar.gz /app/
ENV FLASK_SECRET=super-secret-key
EXPOSE 5000
CMD flask run --host=0.0.0.0
Ce que tu dois corriger :
- Tag imprécis (
latest) - Layers éclatés (3
RUNséparés) - Secret en dur dans
ENV ADDau lieu deCOPYpour un cas simple- Absence de
WORKDIR - Pas d’utilisateur non-root
- Forme shell du
CMD(pas de gestion des signaux) - Pas de
.dockerignore pip installsans--no-cache-dir
Réécris ce Dockerfile en version production-ready, ajoute un .dockerignore, et vérifie avec docker history que tes layers sont propres.
🧠 À retenir : Un bon Dockerfile, c’est comme du bon code — il est lisible, maintenable, et chaque ligne a une raison d’être. Les multi-stage builds séparent le build du runtime. Le cache est ton meilleur allié si tu respectes l’ordre des instructions. Et la sécurité commence par un utilisateur non-root et zéro secret dans l’image.
🖥️ Pratique sur ton propre serveur
Pour suivre Apprendre Docker en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.
Contenu réservé aux abonnés
Ce chapitre fait partie de la formation complète. Abonne-toi pour débloquer tous les contenus.
Débloquer pour 29 CHF/moisLe chapitre 1 de chaque formation est gratuit.
Série pas encore débloquée
Termine la série prérequise d'abord pour accéder à ce contenu.
Aller à la série prérequiseSérie : Apprendre Docker
4 / 10- Pourquoi Docker ? Les conteneurs expliqués
- Tes premières commandes Docker
- Ton premier Dockerfile
- 4 Dockerfile avancé : multi-stage et optimisation
- 5 Docker Compose : orchestrer tes services
- 6 Compose avancé : volumes, réseaux et scaling
- 7 Docker en production : logs et healthchecks
- 8 Déploiement et CI/CD avec Docker
- 9 Sécurité Docker : rootless et isolation
- 10 Scanning et conformité Docker
Sur cette page
Articles liés
Pourquoi Docker ? Les conteneurs expliqués
Découvre ce que sont les conteneurs, comment ils se distinguent des machines virtuelles et comprends l'architecture Docker.
Tes premières commandes Docker
Prends en main Docker avec tes premières commandes : docker run, pull, ps, images, exec et le cycle de vie des conteneurs.
Ton premier Dockerfile
Apprends à écrire un Dockerfile avec les instructions essentielles : FROM, COPY, RUN, CMD et docker build.