Docker Best Practices : 16 Bonnes Pratiques pour des Conteneurs Production-Ready
dockerintermediate

Docker Best Practices : 16 Bonnes Pratiques pour des Conteneurs Production-Ready

Guide complet des bonnes pratiques Docker pour optimiser vos Dockerfiles, sécuriser vos conteneurs et réduire la taille de vos images. Exemples concrets et astuces production.

Antoine C
10 min read
#docker#best-practices#dockerfile#security#performance#devops

Docker Best Practices : 16 Bonnes Pratiques pour des Conteneurs Production-Ready

La majorité des Dockerfiles que je revois en code review partagent les mêmes problèmes : images de 2 GB alors que 200 MB suffiraient, builds de 15 minutes qui pourraient prendre 30 secondes, conteneurs qui tournent en root sans raison valable.

Le conteneur fonctionne en local, les tests passent, mais une fois en production, les problèmes s'accumulent : temps de déploiement excessifs, failles de sécurité, consommation mémoire explosive.

La différence entre un Dockerfile qui "marche" et un Dockerfile production-ready tient souvent à une quinzaine de bonnes pratiques. Ces pratiques ne sont pas compliquées, mais elles demandent de comprendre comment Docker fonctionne sous le capot : système de layers, cache de build, isolation des processus.

Dans cet article, vous allez découvrir les 16 bonnes pratiques Docker essentielles pour passer d'images amateurs à des conteneurs professionnels. On couvrira l'optimisation des Dockerfiles, la sécurité, la performance, et les patterns de production.

Cet article présente chaque pratique de manière concise. Pour aller plus loin et pratiquer sur des environnements réels, Train With Docker propose des scénarios "Docker Best Practices" qui vous guident pas à pas dans l'implémentation de chaque concept.

Optimisation des Dockerfiles#

1. Utilisez les multi-stage builds#

Le multi-stage build est probablement la technique la plus impactante pour réduire la taille de vos images. L'idée : séparer l'environnement de build de l'environnement d'exécution.

dockerfile
# Stage 1 : Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 : Production
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]

Le premier stage contient tout le nécessaire pour compiler (TypeScript, devDependencies, sources). Le second stage ne conserve que le strict minimum : le code compilé et les dépendances de production.

Gain typique

Un projet Node.js TypeScript passe souvent de 1.2 GB à 150 MB avec un multi-stage build bien configuré.

2. Ordonnez vos instructions pour maximiser le cache#

Docker cache chaque layer. Quand une instruction change, toutes les instructions suivantes sont reconstruites. L'ordre de vos instructions impacte donc directement la vitesse de vos builds.

dockerfile
# Mauvais : le cache est invalidé à chaque changement de code
COPY . .
RUN npm ci
RUN npm run build

# Bon : les dépendances sont cachées séparément
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

La règle : placez les éléments qui changent rarement en premier (dépendances système, puis packages, puis code source).

3. Minimisez le nombre de layers#

Chaque instruction RUN, COPY, et ADD crée un nouveau layer. Combinez les commandes liées pour réduire le nombre de layers.

dockerfile
# Mauvais : 3 layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get clean

# Bon : 1 layer
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
Prêt à Essayer par Vous-Même ?
Pratiquez ces concepts Docker dans un environnement réel avec des scénarios pratiques.

4. Utilisez un fichier .dockerignore#

Le fichier .dockerignore fonctionne comme .gitignore : il exclut des fichiers du contexte de build. Sans lui, Docker copie tout, y compris node_modules, .git, et vos fichiers de test.

text
node_modules
.git
.gitignore
*.md
.env*
coverage
.nyc_output
dist
Attention

Un contexte de build volumineux ralentit chaque build, même si vous ne copiez pas ces fichiers explicitement. Docker doit quand même les envoyer au daemon.

5. Choisissez la bonne image de base#

Le choix de l'image de base impacte la taille finale, la sécurité, et les dépendances disponibles.

ImageTailleCas d'usage
node:20~400 MBJamais en production
node:20-slim~75 MBQuand vous avez besoin de packages système
node:20-alpine~50 MBChoix par défaut
gcr.io/distroless/nodejs20~188 MBSécurité maximale

Alpine utilise musl libc au lieu de glibc. Dans 95% des cas, ça fonctionne parfaitement. Pour les 5% restants (certains binaires natifs), utilisez -slim.

Les images distroless de Google sont plus lourdes qu'Alpine, mais elles ne contiennent ni shell, ni gestionnaire de paquets, ni aucun outil système. Seul le runtime (ici Node.js) et ses dépendances sont présents. Résultat : moins de surface d'attaque et aucun moyen pour un attaquant d'exécuter des commandes dans le conteneur.

Sécurité des conteneurs#

6. Ne tournez jamais en root#

Par défaut, les conteneurs Docker tournent en root. C'est un problème de sécurité majeur : si un attaquant compromet votre application, il a les privilèges root dans le conteneur.

dockerfile
FROM node:20-alpine

# Créez un utilisateur non-root
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

WORKDIR /app
COPY --chown=nextjs:nodejs . .

# Changez d'utilisateur avant CMD
USER nextjs

CMD ["node", "index.js"]
Vérification

Vous pouvez vérifier l'utilisateur courant avec docker exec <container> whoami. Si ça retourne root, vous avez un problème.

7. Limitez les capabilities Linux#

Par défaut, Docker accorde à vos conteneurs un ensemble de capabilities Linux (permissions système). La plupart sont inutiles pour une application standard et représentent un risque de sécurité.

bash
# Supprimez toutes les capabilities et ajoutez uniquement celles nécessaires
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp

Les capabilities les plus courantes :

CapabilityUsage
NET_BIND_SERVICEÉcouter sur un port < 1024
CHOWNChanger le propriétaire des fichiers
SETUID / SETGIDChanger d'utilisateur/groupe
SYS_PTRACEDebugger des processus (éviter en prod)
Principe du moindre privilège

Commencez toujours par --cap-drop=ALL, puis ajoutez uniquement les capabilities dont votre application a réellement besoin. Si votre app plante, les logs vous indiqueront quelle capability manque.

Pour Docker Compose :

yaml
services:
  app:
    image: myapp
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
Passez de la Théorie à la Pratique
Appliquez ce que vous avez appris avec des scénarios Docker interactifs et des environnements réels.

8. Utilisez des images de base minimales#

Plus une image contient de packages, plus elle a de surface d'attaque. Les images distroless de Google ne contiennent que votre application et ses dépendances runtime, sans shell ni outils système.

dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY . .
RUN npm ci && npm run build

FROM gcr.io/distroless/nodejs20-debian12
COPY --from=builder /app/dist /app
WORKDIR /app
CMD ["index.js"]

L'inconvénient : impossible de faire docker exec pour debugger. C'est voulu. En production, vous ne devriez pas avoir besoin d'un shell dans vos conteneurs.

9. Scannez vos images pour les vulnérabilités#

Les images Docker peuvent contenir des CVE (vulnérabilités connues) dans leurs packages système ou dépendances.

bash
# Avec Docker Scout (intégré à Docker Desktop)
docker scout cves <image>

# Avec Trivy (open source)
trivy image <image>

# Avec Snyk
snyk container test <image>

Intégrez ces scans dans votre CI/CD. Un scan qui découvre une vulnérabilité critique devrait bloquer le déploiement.

10. Gérez vos secrets correctement#

Ne mettez jamais de secrets dans votre Dockerfile ou votre image. Ils resteront dans l'historique des layers.

dockerfile
# JAMAIS : le secret reste dans l'image
ENV DATABASE_URL=postgres://user:password@host/db

# MIEUX : utilisez les secrets Docker (build-time)
# Le secret est monté temporairement dans /run/secrets/ pendant le build
# Il n'est jamais écrit dans un layer de l'image
RUN --mount=type=secret,id=db_url \
    export DATABASE_URL=$(cat /run/secrets/db_url) && \
    npm run migrate

# Pour builder avec ce secret :
# docker build --secret id=db_url,src=.env .

# EN PRODUCTION : passez les secrets au runtime
docker run -e DATABASE_URL=$DATABASE_URL myapp

Pour Docker Swarm et Kubernetes, utilisez leurs mécanismes natifs de secrets.

Performance et taille des images#

11. Nettoyez les caches dans le même layer#

Les gestionnaires de paquets laissent des caches. Si vous les nettoyez dans un layer séparé, le cache reste dans les layers précédents.

dockerfile
# Python
RUN pip install --no-cache-dir -r requirements.txt

# Node.js
RUN npm ci --only=production && npm cache clean --force

# Debian/Ubuntu
RUN apt-get update && \
    apt-get install -y --no-install-recommends package && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
Maîtrisez Docker en Pratique
Allez au-delà de la théorie - pratiquez avec de vrais conteneurs et scénarios d'orchestration.

12. Utilisez COPY au lieu de ADD#

COPY fait exactement ce que son nom indique : copier des fichiers. ADD fait la même chose, mais extrait automatiquement les archives (.tar, .gz) et peut télécharger des fichiers depuis une URL. Ces comportements implicites rendent le Dockerfile moins lisible et peuvent introduire des failles de sécurité si une URL est compromise ou si une archive contient des fichiers inattendus. Préférez COPY pour sa prévisibilité.

dockerfile
# Préférez
COPY ./src /app/src

# Évitez (sauf si vous avez besoin d'extraire une archive)
ADD ./src /app/src

Bonnes pratiques de build#

13. Ajoutez des labels pour les métadonnées#

Les labels permettent d'identifier et de documenter vos images.

dockerfile
LABEL org.opencontainers.image.title="Mon Application"
LABEL org.opencontainers.image.description="API backend"
LABEL org.opencontainers.image.version="1.2.3"
LABEL org.opencontainers.image.authors="[email protected]"
LABEL org.opencontainers.image.source="https://github.com/org/repo"

Ces labels suivent la spécification OCI et sont reconnus par Docker Hub, GitHub Container Registry, et d'autres registries.

14. Implémentez des health checks#

Un health check permet à Docker (et aux orchestrateurs) de vérifier que votre application fonctionne réellement.

dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:3000/health || exit 1

Sans health check, Docker considère un conteneur "healthy" dès qu'il démarre, même si l'application plante au bout de 5 secondes.

15. Comprenez la différence entre ENTRYPOINT et CMD#

  • ENTRYPOINT : définit la commande principale (rarement overridée)
  • CMD : définit les arguments par défaut (facilement overridés)
dockerfile
# Pattern recommandé
ENTRYPOINT ["node"]
CMD ["index.js"]

# Permet de faire :
docker run myapp                    # Exécute node index.js
docker run myapp other-script.js    # Exécute node other-script.js

Utilisez la forme exec ["cmd", "arg"] plutôt que la forme shell cmd arg. La forme exec permet une meilleure gestion des signaux.

Logging et observabilité#

16. Écrivez vos logs sur stdout/stderr#

Docker capture automatiquement tout ce qui sort sur stdout et stderr. N'écrivez pas dans des fichiers de log.

javascript
// Bon : stdout
console.log(JSON.stringify({ level: 'info', message: 'User logged in', userId: 123 }));

// Mauvais : fichier
fs.appendFileSync('/var/log/app.log', 'User logged in\n');

Le format JSON structure vos logs pour les outils d'agrégation (ELK, Datadog, CloudWatch).

dockerfile
# Ne redirigez pas les logs dans des fichiers
CMD ["node", "index.js"]

# Pas ça :
CMD ["node", "index.js", ">", "/var/log/app.log"]

Orchestration et production#

Une fois vos images optimisées, quelques pratiques supplémentaires s'appliquent au runtime.

Limitez les ressources pour éviter qu'un conteneur monopolise le serveur :

bash
docker run --memory="512m" --cpus="0.5" myapp

Configurez les restart policies pour la résilience :

bash
docker run --restart=unless-stopped myapp

Utilisez des rolling updates avec Docker Swarm ou Kubernetes pour déployer sans interruption de service.

Docker Swarm

Si vous préparez la certification DCA ou si vous voulez approfondir Docker Swarm, Train With Docker propose des scénarios pratiques avec des clusters préconfigurés.

Conclusion#

Ces 16 bonnes pratiques Docker couvrent les aspects essentiels : optimisation des builds, sécurité, performance, et production-readiness. Appliquées systématiquement, elles transforment des Dockerfiles amateurs en configurations professionnelles.

Le plus important : comprendre pourquoi chaque pratique existe. Le multi-stage build réduit la taille parce qu'il sépare build et runtime. Le cache fonctionne par layer, donc l'ordre compte. Les conteneurs non-root limitent l'impact d'une compromission.

Cet article survole chaque pratique sans entrer dans les détails d'implémentation. Pour mettre les mains dans le cambouis, Train With Docker propose des scénarios "Docker Best Practices" qui vous permettent de pratiquer chaque concept sur des environnements préconfigurés, sans rien installer localement.

Docker Best Practices : 16 Bonnes Pratiques pour des Conteneurs Production-Ready