Aller au contenu
Conteneurs & Orchestration high

Dockerfile : bonnes pratiques (cache, .dockerignore, multi-stage, non-root)

18 min de lecture

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.

Pensez à un Dockerfile comme une recette de cuisine industrielle :

Concept DockerfileÉquivalent cuisine
Cache des couchesVous ne remesurez pas les ingrédients de base si vous avez déjà la pâte prête
.dockerignoreVous n’apportez pas tout le frigo en cuisine, juste ce qu’il faut
Multi-stageAtelier de préparation ≠ assiette finale (on ne sert pas les épluchures)
USER non-rootLe chef ne donne pas les clés de la cuisine à tout le monde
HEALTHCHECKGoûter le plat avant de servir
  • 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.

Comparaison de l'ordre des couches Dockerfile : mauvais ordre (tout invalidé) vs bon ordre (cache préservé)

❌ Mauvais : le cache est invalidé à chaque changement de code

Section intitulée « ❌ Mauvais : le cache est invalidé à chaque changement de code »
FROM node:20-alpine
WORKDIR /app
COPY . . # ← Invalide le cache à chaque changement
RUN npm install # ← Réexécuté à chaque build
EXPOSE 3000
CMD ["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-alpine
WORKDIR /app
COPY package*.json ./ # ← Change rarement
RUN npm install # ← En cache si package.json n'a pas changé
COPY . . # ← Change souvent, mais npm install est déjà fait
EXPOSE 3000
CMD ["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 rarementCe qui change souvent
Image de base (FROM)Code source
Installation d’outils systèmeFichiers de configuration
Dépendances (package.json, go.mod)Assets statiques

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 locaux
dist/
build/
*.log
# Secrets et config locale (CRITIQUE : risque d'exfiltration)
.env
.env.local
*.pem
*.key
secrets/
.npmrc
.pypirc
kubeconfig
# Docker (optionnel — à exclure si remote build)
# Dockerfile*
# docker-compose*.yml
# .dockerignore

Vérifier ce qui est envoyé au daemon :

Fenêtre de terminal
# Affiche la taille du contexte
docker 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).

Schéma multi-stage build : stage build (SDK ~1 Go) → copie artefact → stage runtime (Alpine ~10 Mo)

# Stage 1 : 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 /app/main .
# Stage 2 : Runtime (image finale)
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/main /main
USER nobody
ENTRYPOINT ["/main"]
StageContenuTaille
builderGo SDK + sources + dépendances~1 Go
finalAlpine + binaire compilé~10 Mo
# Stage 1 : Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2 : Runtime
FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/index.js"]

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-root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Basculer vers l'utilisateur non-root
USER appuser
EXPOSE 3000
CMD ["node", "index.js"]

Images avec utilisateur intégré : certaines images de base fournissent déjà un utilisateur non-root :

ImageUtilisateurComment l’utiliser
node:*node (UID 1000)USER node
nginx:*nginxUSER nginx
python:*Créer manuellement

Un healthcheck permet à Docker de vérifier si votre application fonctionne correctement.

FROM nginx:alpine
# Healthcheck : vérifie que nginx répond
HEALTHCHECK --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/
OptionSignification
--interval=30sVérifie toutes les 30 secondes
--timeout=3sLa commande doit répondre en 3 secondes
--start-period=5sAttend 5s avant le premier check (temps de démarrage)
--retries=33 échecs consécutifs = unhealthy

Voir l’état de santé :

Fenêtre de terminal
docker ps
# CONTAINER ID STATUS
# abc123 Up 5 minutes (healthy)
docker inspect --format '{{.State.Health.Status}}' mon_conteneur
# healthy / unhealthy / starting

Voici un Dockerfile qui applique toutes les bonnes pratiques :

# syntax=docker/dockerfile:1
# Stage 1 : Build
FROM node:20-alpine AS builder
WORKDIR /app
# 1. Copier d'abord les fichiers de dépendances (cache)
COPY package*.json ./
RUN npm ci
# 2. Puis le code source
COPY . .
RUN npm run build
# Stage 2 : Runtime
FROM node:20-alpine
# 3. Métadonnées
LABEL org.opencontainers.image.source="https://github.com/mon-org/mon-app"
LABEL org.opencontainers.image.description="Mon application"
WORKDIR /app
ENV NODE_ENV=production
# 4. Dépendances de production uniquement
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
# 5. Copier le build depuis le stage précédent
COPY --from=builder --chown=node:node /app/dist ./dist
# 6. Utilisateur non-root
USER node
# 7. Healthcheck
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
EXPOSE 3000
CMD ["node", "dist/index.js"]

BuildKit permet de monter des caches pendant le build, évitant de retélécharger les dépendances :

# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Cache npm entre les builds
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
COPY . .
# Python (pip)
RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt
# Go modules
RUN --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

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_TOKEN
RUN npm install
# ✅ Secret monté temporairement (BuildKit) — jamais dans l'image finale
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --omit=dev

Comment passer le secret au build :

Fenêtre de terminal
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.

Évitez les tags flottants comme latest ou 20 — ils peuvent changer sans préavis et casser vos builds :

# ❌ Peut changer à tout moment
FROM 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écisionExempleReproductibilitéPatchs sécu
Tag majeurnode:20⚠️ Faible✅ Auto
Tag mineurnode:20.11-alpine🔶 Moyenne🔶 Mineurs
Tag + digestnode:20.11-alpine@sha256:...✅ Maximale❌ Manuel

Symptôme : le cache n’est jamais utilisé, même si package.json n’a pas changé.

Causes possibles :

  1. BuildKit désactivé : Docker sans BuildKit a un cache moins intelligent

    Fenêtre de terminal
    export DOCKER_BUILDKIT=1
    docker build .
  2. Contexte de build trop large : vérifiez avec docker build . 2>&1 | head -1

    • Si > 100 Mo, votre .dockerignore est incomplet
  3. COPY avec wildcard mal placé : COPY *.json ./ peut inclure des fichiers inattendus

    • Soyez explicite : COPY package.json package-lock.json ./

Symptôme : l’image finale fait plusieurs centaines de Mo malgré le multi-stage.

Causes possibles :

  1. Mauvaise image de base runtime : utilisez alpine, slim ou distroless

    # ❌ 900 Mo
    FROM node:20
    # ✅ 180 Mo
    FROM node:20-alpine
    # ✅✅ 130 Mo
    FROM node:20-slim
  2. Fichiers de dev copiés : vérifiez que vous copiez uniquement /dist ou /build

  3. Cache npm/pip dans l’image : nettoyez après installation

    RUN npm ci --omit=dev && npm cache clean --force

Symptôme : le conteneur crash avec EACCES ou permission denied.

Causes possibles :

  1. Fichiers appartenant à root : utilisez COPY --chown

    COPY --chown=node:node . .
  2. Dossier de travail non accessible : créez le dossier avant de changer d’utilisateur

    WORKDIR /app
    RUN chown node:node /app
    USER node
  3. Port privilégié : les ports < 1024 nécessitent root

    # ❌ Port 80 nécessite root
    EXPOSE 80
    # ✅ Utilisez un port > 1024
    EXPOSE 3000

Symptôme : le conteneur démarre mais reste en état “unhealthy”.

Causes possibles :

  1. Endpoint incorrect : vérifiez que l’URL existe

    Fenêtre de terminal
    docker exec mon_conteneur wget -q --spider http://localhost:3000/health
  2. start-period trop court : l’application n’a pas le temps de démarrer

    HEALTHCHECK --start-period=30s ... # Augmentez si nécessaire
  3. Outil manquant dans l’image : curl ou wget non installé

    # Alpine : installer wget
    RUN apk add --no-cache wget

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

7 questions
5 min.
80%

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

  1. Ordre des instructions : dépendances avant code source pour maximiser le cache
  2. .dockerignore : exclure node_modules, .git, secrets, fichiers de build
  3. Multi-stage : séparer build et runtime, copier uniquement le nécessaire
  4. USER non-root : toujours basculer vers un utilisateur non privilégié
  5. HEALTHCHECK : permettre à Docker de vérifier la santé de l’application
  6. COPY --chown : attribuer les bons droits dès la copie

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.