Vos builds Docker prennent 10 minutes à chaque changement de code ? C’est probablement un problème d’ordre des instructions dans votre Dockerfile. Ce guide vous donne les bonnes pratiques essentielles pour écrire des Dockerfiles optimisés : cache efficace, images légères, sécurité de base. Des principes simples qui vous feront gagner du temps au quotidien.
Analogie : la recette de cuisine
Section intitulée « Analogie : la recette de cuisine »Pensez à un Dockerfile comme une recette de cuisine industrielle :
| Concept Dockerfile | Équivalent cuisine |
|---|---|
| Cache des couches | Vous ne remesurez pas les ingrédients de base si vous avez déjà la pâte prête |
| .dockerignore | Vous n’apportez pas tout le frigo en cuisine, juste ce qu’il faut |
| Multi-stage | Atelier de préparation ≠ assiette finale (on ne sert pas les épluchures) |
| USER non-root | Le chef ne donne pas les clés de la cuisine à tout le monde |
| HEALTHCHECK | Goûter le plat avant de servir |
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Ordre des instructions : structurer pour maximiser le cache
- .dockerignore : exclure les fichiers inutiles du contexte de build
- Multi-stage build : séparer build et runtime pour des images légères
- Utilisateur non-root : réduire la surface d’attaque
- Healthcheck : permettre à Docker de vérifier la santé du conteneur
Principe #1 : Maximiser le cache avec l’ordre des instructions
Section intitulée « Principe #1 : Maximiser le cache avec l’ordre des instructions »Docker construit une image couche par couche. Quand une couche change, toutes les couches suivantes sont reconstruites. L’ordre des instructions dans le Dockerfile est donc crucial.
La règle d’or : placer les instructions qui changent rarement en haut, celles qui changent souvent en bas.
❌ Mauvais : le cache est invalidé à chaque changement de code
Section intitulée « ❌ Mauvais : le cache est invalidé à chaque changement de code »FROM node:20-alpineWORKDIR /appCOPY . . # ← Invalide le cache à chaque changementRUN npm install # ← Réexécuté à chaque buildEXPOSE 3000CMD ["npm", "start"]✅ Bon : seul le code change, les dépendances sont en cache
Section intitulée « ✅ Bon : seul le code change, les dépendances sont en cache »FROM node:20-alpineWORKDIR /appCOPY package*.json ./ # ← Change rarementRUN npm install # ← En cache si package.json n'a pas changéCOPY . . # ← Change souvent, mais npm install est déjà faitEXPOSE 3000CMD ["npm", "start"]Gain typique : réduction significative du temps de build (souvent d’un ordre de grandeur), selon la taille des dépendances et la bande passante.
| Ce qui change rarement | Ce qui change souvent |
|---|---|
Image de base (FROM) | Code source |
| Installation d’outils système | Fichiers de configuration |
| Dépendances (package.json, go.mod) | Assets statiques |
Principe #2 : Utiliser .dockerignore
Section intitulée « Principe #2 : Utiliser .dockerignore »Le fichier .dockerignore empêche Docker de copier des fichiers inutiles dans le contexte de build. Moins de fichiers = builds plus rapides + images plus légères + moins de risques de fuite de secrets.
Créez .dockerignore à la racine du projet :
# Dépendances (réinstallées dans l'image)node_modules/vendor/__pycache__/*.pyc
# Git et IDE.git/.gitignore.vscode/.idea/*.swp
# Fichiers de build locauxdist/build/*.log
# Secrets et config locale (CRITIQUE : risque d'exfiltration).env.env.local*.pem*.keysecrets/.npmrc.pypirckubeconfig
# Docker (optionnel — à exclure si remote build)# Dockerfile*# docker-compose*.yml# .dockerignoreVérifier ce qui est envoyé au daemon :
# Affiche la taille du contextedocker build . 2>&1 | head -1# "Sending build context to Docker daemon 2.048kB" ← Bon# "Sending build context to Docker daemon 500MB" ← Problème !Principe #3 : Multi-stage build pour des images légères
Section intitulée « Principe #3 : Multi-stage build pour des images légères »Un multi-stage build utilise plusieurs FROM dans le même Dockerfile. Chaque stage est indépendant. Le stage final ne copie que les artefacts nécessaires des stages précédents.
L’idée : séparer l’environnement de build (compilateur, outils de dev, dépendances de build) de l’environnement de runtime (juste l’exécutable).
Exemple Go : de 1 Go à 10 Mo
Section intitulée « Exemple Go : de 1 Go à 10 Mo »# Stage 1 : BuildFROM golang:1.21-alpine AS builderWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o /app/main .
# Stage 2 : Runtime (image finale)FROM alpine:3.19RUN apk add --no-cache ca-certificatesCOPY --from=builder /app/main /mainUSER nobodyENTRYPOINT ["/main"]| Stage | Contenu | Taille |
|---|---|---|
| builder | Go SDK + sources + dépendances | ~1 Go |
| final | Alpine + binaire compilé | ~10 Mo |
Exemple Node.js : séparation build / prod
Section intitulée « Exemple Node.js : séparation build / prod »# Stage 1 : BuildFROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
# Stage 2 : RuntimeFROM node:20-alpineWORKDIR /appENV NODE_ENV=productionCOPY package*.json ./RUN npm ci --omit=dev && npm cache clean --forceCOPY --from=builder /app/dist ./distUSER nodeEXPOSE 3000CMD ["node", "dist/index.js"]Principe #4 : Exécuter en non-root
Section intitulée « Principe #4 : Exécuter en non-root »Par défaut, les processus dans un conteneur tournent en root. Si un attaquant compromet l’application, il a les privilèges root dans le conteneur.
La solution : créer un utilisateur dédié et l’utiliser via USER.
FROM node:20-alpine
# Créer un utilisateur non-rootRUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /appCOPY --chown=appuser:appgroup . .
# Basculer vers l'utilisateur non-rootUSER appuser
EXPOSE 3000CMD ["node", "index.js"]Images avec utilisateur intégré : certaines images de base fournissent déjà un utilisateur non-root :
| Image | Utilisateur | Comment l’utiliser |
|---|---|---|
node:* | node (UID 1000) | USER node |
nginx:* | nginx | USER nginx |
python:* | — | Créer manuellement |
Principe #5 : Ajouter un healthcheck
Section intitulée « Principe #5 : Ajouter un healthcheck »Un healthcheck permet à Docker de vérifier si votre application fonctionne correctement.
FROM nginx:alpine
# Healthcheck : vérifie que nginx répondHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost/ || exit 1
COPY index.html /usr/share/nginx/html/| Option | Signification |
|---|---|
--interval=30s | Vérifie toutes les 30 secondes |
--timeout=3s | La commande doit répondre en 3 secondes |
--start-period=5s | Attend 5s avant le premier check (temps de démarrage) |
--retries=3 | 3 échecs consécutifs = unhealthy |
Voir l’état de santé :
docker ps# CONTAINER ID STATUS# abc123 Up 5 minutes (healthy)
docker inspect --format '{{.State.Health.Status}}' mon_conteneur# healthy / unhealthy / startingRécapitulatif : Dockerfile optimisé
Section intitulée « Récapitulatif : Dockerfile optimisé »Voici un Dockerfile qui applique toutes les bonnes pratiques :
# syntax=docker/dockerfile:1
# Stage 1 : BuildFROM node:20-alpine AS builderWORKDIR /app
# 1. Copier d'abord les fichiers de dépendances (cache)COPY package*.json ./RUN npm ci
# 2. Puis le code sourceCOPY . .RUN npm run build
# Stage 2 : RuntimeFROM node:20-alpine
# 3. MétadonnéesLABEL org.opencontainers.image.source="https://github.com/mon-org/mon-app"LABEL org.opencontainers.image.description="Mon application"
WORKDIR /appENV NODE_ENV=production
# 4. Dépendances de production uniquementCOPY package*.json ./RUN npm ci --omit=dev && npm cache clean --force
# 5. Copier le build depuis le stage précédentCOPY --from=builder --chown=node:node /app/dist ./dist
# 6. Utilisateur non-rootUSER node
# 7. HealthcheckHEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000CMD ["node", "dist/index.js"]Bonnes pratiques avancées
Section intitulée « Bonnes pratiques avancées »Utiliser les cache mounts BuildKit
Section intitulée « Utiliser les cache mounts BuildKit »BuildKit permet de monter des caches pendant le build, évitant de retélécharger les dépendances :
# syntax=docker/dockerfile:1FROM node:20-alpineWORKDIR /appCOPY package*.json ./
# Cache npm entre les buildsRUN --mount=type=cache,target=/root/.npm \ npm ci --omit=dev
COPY . .Cache mounts pour d’autres langages
Section intitulée « Cache mounts pour d’autres langages »# Python (pip)RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir -r requirements.txt
# Go modulesRUN --mount=type=cache,target=/go/pkg/mod \ go build -o /app/main .
# Debian/Ubuntu (apt)RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt \ apt-get update && apt-get install -y --no-install-recommends curlÉviter les secrets dans l’image
Section intitulée « Éviter les secrets dans l’image »Ne jamais passer de secrets via ARG ou ENV — ils restent dans l’historique des couches et sont visibles avec docker history :
# ❌ Secret visible dans l'image (même si ARG est "privé")ARG NPM_TOKENRUN npm install
# ✅ Secret monté temporairement (BuildKit) — jamais dans l'image finaleRUN --mount=type=secret,id=npmrc,target=/root/.npmrc \ npm ci --omit=devComment passer le secret au build :
docker build --secret id=npmrc,src=$HOME/.npmrc .Le fichier .npmrc est disponible pendant le RUN, mais n’apparaît pas dans les couches de l’image finale.
Fixer les versions des images de base
Section intitulée « Fixer les versions des images de base »Évitez les tags flottants comme latest ou 20 — ils peuvent changer sans préavis et casser vos builds :
# ❌ Peut changer à tout momentFROM node:20-alpine
# ✅ Version mineure + variante (bon compromis)FROM node:20.11-alpine
# ✅✅ Digest pour la reproductibilité maximale (supply chain critique)FROM node:20.11-alpine@sha256:abc123...| Niveau de précision | Exemple | Reproductibilité | Patchs sécu |
|---|---|---|---|
| Tag majeur | node:20 | ⚠️ Faible | ✅ Auto |
| Tag mineur | node:20.11-alpine | 🔶 Moyenne | 🔶 Mineurs |
| Tag + digest | node:20.11-alpine@sha256:... | ✅ Maximale | ❌ Manuel |
Dépannage
Section intitulée « Dépannage »Build toujours lent malgré le bon ordre
Section intitulée « Build toujours lent malgré le bon ordre »Symptôme : le cache n’est jamais utilisé, même si package.json n’a pas changé.
Causes possibles :
-
BuildKit désactivé : Docker sans BuildKit a un cache moins intelligent
Fenêtre de terminal export DOCKER_BUILDKIT=1docker build . -
Contexte de build trop large : vérifiez avec
docker build . 2>&1 | head -1- Si > 100 Mo, votre
.dockerignoreest incomplet
- Si > 100 Mo, votre
-
COPY avec wildcard mal placé :
COPY *.json ./peut inclure des fichiers inattendus- Soyez explicite :
COPY package.json package-lock.json ./
- Soyez explicite :
Image multi-stage toujours volumineuse
Section intitulée « Image multi-stage toujours volumineuse »Symptôme : l’image finale fait plusieurs centaines de Mo malgré le multi-stage.
Causes possibles :
-
Mauvaise image de base runtime : utilisez
alpine,slimoudistroless# ❌ 900 MoFROM node:20# ✅ 180 MoFROM node:20-alpine# ✅✅ 130 MoFROM node:20-slim -
Fichiers de dev copiés : vérifiez que vous copiez uniquement
/distou/build -
Cache npm/pip dans l’image : nettoyez après installation
RUN npm ci --omit=dev && npm cache clean --force
Erreur “permission denied” avec USER non-root
Section intitulée « Erreur “permission denied” avec USER non-root »Symptôme : le conteneur crash avec EACCES ou permission denied.
Causes possibles :
-
Fichiers appartenant à root : utilisez
COPY --chownCOPY --chown=node:node . . -
Dossier de travail non accessible : créez le dossier avant de changer d’utilisateur
WORKDIR /appRUN chown node:node /appUSER node -
Port privilégié : les ports < 1024 nécessitent root
# ❌ Port 80 nécessite rootEXPOSE 80# ✅ Utilisez un port > 1024EXPOSE 3000
HEALTHCHECK toujours “unhealthy”
Section intitulée « HEALTHCHECK toujours “unhealthy” »Symptôme : le conteneur démarre mais reste en état “unhealthy”.
Causes possibles :
-
Endpoint incorrect : vérifiez que l’URL existe
Fenêtre de terminal docker exec mon_conteneur wget -q --spider http://localhost:3000/health -
start-period trop court : l’application n’a pas le temps de démarrer
HEALTHCHECK --start-period=30s ... # Augmentez si nécessaire -
Outil manquant dans l’image :
curlouwgetnon installé# Alpine : installer wgetRUN apk add --no-cache wget
Testez vos connaissances
Section intitulée « Testez vos connaissances »Vérifions que vous maîtrisez les bonnes pratiques Dockerfile : cache, .dockerignore, multi-stage, non-root et healthcheck.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
📋 Récapitulatif de vos réponses
Vérifiez vos réponses avant de soumettre. Cliquez sur une question pour la modifier.
Détail des réponses
À retenir
Section intitulée « À retenir »- Ordre des instructions : dépendances avant code source pour maximiser le cache
- .dockerignore : exclure node_modules, .git, secrets, fichiers de build
- Multi-stage : séparer build et runtime, copier uniquement le nécessaire
- USER non-root : toujours basculer vers un utilisateur non privilégié
- HEALTHCHECK : permettre à Docker de vérifier la santé de l’application
COPY --chown: attribuer les bons droits dès la copie
Pour aller plus loin
Section intitulée « Pour aller plus loin »Prochaine étape
Section intitulée « Prochaine étape »Maintenant que vous savez écrire des Dockerfiles optimisés, apprenez à industrialiser vos builds en CI/CD avec une stratégie de tags, cache et multi-arch.