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.

  • 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
# ← Invalide le cache à chaque changement
COPY . .
# ← Réexécuté à chaque build
RUN npm install
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@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
WORKDIR /app
# ← Change rarement
COPY package*.json ./
# ← En cache si package.json n'a pas changé
RUN npm install
# ← Change souvent, mais npm install est déjà fait
COPY . .
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@sha256:6baf43584bcb78f2e5847d1de515f23499913ac9f12bdf834811a3145eb11ca1
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@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
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@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
# 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:1.31.3-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
# 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@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
# 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@sha256:fb4cd12c85ee03686f6af5362a0b0d56d50c58a04632e6c0fb8363f609372293
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

    WORKDIR /app
    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% requis

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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn