Docker en production : les erreurs que je vois le plus souvent

Conteneurs root, images non patchées, volumes mal montés... Tour d'horizon des erreurs classiques et comment les éviter dans vos déploiements Docker.

Depuis plusieurs années, j'accompagne des équipes qui déploient leurs applications avec Docker Kanvas pour migrer vers Kubernetes. Que ce soit des startups ou des structures plus établies, je retrouve régulièrement les mêmes erreurs. Certaines sont bénignes, d'autres peuvent coûter cher : failles de sécurité, pannes en cascade, données perdues.

Cet article n'est pas un tutoriel Docker -- si vous cherchez ça, consultez plutôt mon guide complet Docker. Ici, je partage un retour d'expérience terrain sur les sept erreurs que je croise le plus souvent en monitoring Linux en production, et comment les corriger concrètement.

1. Des conteneurs qui tournent en root

C'est de loin l'erreur la plus fréquente. Par défaut, Docker exécute les processus dans le conteneur en tant que root. Beaucoup d'équipes ne changent jamais ce comportement, souvent parce que "ça marche comme ça".

Un conteneur root qui subit une évasion (container escape) donne à l'attaquant un accès root sur la machine hôte. C'est le scénario catastrophe en sécurité conteneur.

Même sans évasion, un processus root dans le conteneur peut écraser des fichiers montés en volume, modifier la configuration réseau du conteneur, ou consommer des ressources sans restriction.

La correction est simple : créez un utilisateur dédié dans votre Dockerfile et utilisez l'instruction USER.

FROM node:20-alpine

RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app
COPY --chown=appuser:appgroup . .
RUN npm ci --only=production

USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
Bonne pratique : testez que votre conteneur fonctionne correctement avec un utilisateur non-root dès le développement. Attendre la mise en production pour corriger ça génère toujours des surprises de permissions.

2. Utiliser le tag :latest en production

Le tag :latest est un piège classique. Il ne désigne pas "la dernière version stable" mais simplement "le dernier build poussé sans tag explicite". Deux déploiements identiques à une heure d'intervalle peuvent produire des conteneurs différents si l'image a été mise à jour entre-temps.

Avec `:latest`, vous perdez toute reproductibilité. Impossible de savoir quelle version tourne réellement, impossible de revenir en arrière proprement en cas de régression.

Utilisez toujours un tag précis, idéalement le digest SHA256 pour les environnements critiques.

services:
  api:
    # Mauvais : imprevisible
    # image: myapp:latest

    # Correct : version explicite
    image: myapp:2.4.1

    # Optimal : immutable par digest
    # image: myapp@sha256:a1b2c3d4e5f6...

Intégrez le tagging dans votre pipeline CI/CD. Chaque build produit une image avec un tag unique (numéro de version, hash de commit, timestamp). C'est la base d'un déploiement fiable.

3. Pas de healthcheck

Docker sait si un conteneur tourne. Il ne sait pas si l'application à l'intérieur fonctionne. Sans healthcheck, un conteneur dont le processus principal est bloqué (deadlock, pool de connexions saturé, mémoire corrompue) reste marqué comme "running" indéfiniment.

L'orchestrateur (Docker Swarm, Kubernetes) ne peut pas prendre de décision intelligente sans cette information. Pas de redémarrage automatique, pas de retrait du load balancer.

FROM python:3.12-slim

COPY . /app
WORKDIR /app

RUN pip install --no-cache-dir -r requirements.txt

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

USER appuser
CMD ["gunicorn", "main:app", "-b", "0.0.0.0:8000"]
Bonne pratique : le endpoint de healthcheck doit vérifier les dépendances critiques (base de données, cache, file d'attente). Un simple "200 OK" statique ne suffit pas à détecter un état dégradé.

4. Des secrets dans les variables d'environnement

Les variables d'environnement sont pratiques pour la configuration, mais elles sont un mauvais choix pour les secrets. Elles apparaissent en clair dans docker inspect, dans les logs de debug, dans les dumps de processus, et souvent dans le code source via les fichiers .env commités par erreur.

J'ai vu des tokens d'API cloud, des mots de passe de bases de données et même des clés privées SSL exposées via un simple docker inspect sur un serveur partagé. Les variables d'environnement ne sont pas un mécanisme de sécurité.

Utilisez Docker Secrets (en Swarm) ou un gestionnaire de secrets externe (Vault, AWS Secrets Manager). En compose, vous pouvez monter les secrets en fichiers.

services:
  api:
    image: myapp:2.4.1
    secrets:
      - db_password
      - api_key
    environment:
      # Configuration non sensible uniquement
      LOG_LEVEL: info
      DB_HOST: postgres

secrets:
  db_password:
    file: ./secrets/db_password.txt
  api_key:
    file: ./secrets/api_key.txt

Dans l'application, lisez le secret depuis le fichier monté dans /run/secrets/. C'est un changement mineur dans le code qui améliore considérablement la posture de sécurité.

5. Volumes mal montés et problèmes de permissions

Les volumes Docker sont essentiels pour persister les données, mais leur montage est une source constante de problèmes. Le cas typique : un conteneur qui tourne avec un utilisateur non-root (comme recommandé au point 1) mais dont le volume est détenu par root sur l'hôte.

Résultat : l'application ne peut ni lire ni écrire ses propres données. Pire, certains corrigent ça avec chmod 777, ce qui revient à ouvrir les portes à tous les processus du système.

FROM python:3.12-slim

RUN groupadd -r appgroup && useradd -r -g appgroup -u 1001 appuser

RUN mkdir -p /app/data && chown -R appuser:appgroup /app/data
VOLUME /app/data

USER appuser
WORKDIR /app
CMD ["python", "main.py"]
services:
  api:
    image: myapp:2.4.1
    user: "1001:1001"
    volumes:
      - app_data:/app/data

volumes:
  app_data:
    driver: local
Bonne pratique : fixez un UID/GID numérique explicite dans le Dockerfile et réutilisez-le dans le compose. Évitez les noms d'utilisateurs qui peuvent varier d'une image à l'autre. Préparez les répertoires avec les bonnes permissions dans un entrypoint si nécessaire.

6. Aucune limite de ressources

Sans limites définies, un seul conteneur peut consommer toute la mémoire ou tout le CPU de la machine hôte. J'ai vu des serveurs de production tomber parce qu'une fuite mémoire dans un conteneur a déclenché l'OOM killer du noyau, qui a tué des processus critiques au hasard -- y compris d'autres conteneurs.

Sans limites de ressources, un conteneur défaillant peut entraîner l'ensemble de l'infrastructure. C'est l'équivalent d'un "noisy neighbor" non contrôlé.
services:
  api:
    image: myapp:2.4.1
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

Les limits définissent le plafond absolu. Les reservations garantissent un minimum disponible pour le conteneur. Dimensionnez ces valeurs en fonction de vos tests de charge, pas au doigt mouillé. Un conteneur qui atteint régulièrement sa limite mémoire a probablement une fuite ou est sous-dimensionné.

7. Des logs qui remplissent le disque

Par défaut, Docker stocke les logs de chaque conteneur dans un fichier JSON sur le disque, sans aucune limite de taille ni rotation. Une application un peu bavarde peut générer des gigaoctets de logs en quelques jours. J'ai diagnostiqué plus d'une panne de production causée par un disque plein à cause des logs Docker.

Un disque plein ne casse pas seulement les logs : il empêche la base de données d'écrire, bloque les déploiements, et peut corrompre des données. C'est une panne silencieuse qui s'aggrave progressivement.

Configurez systématiquement la rotation des logs, soit au niveau du daemon Docker, soit par conteneur.

services:
  api:
    image: myapp:2.4.1
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"
        compress: "true"

Pour une configuration globale, modifiez le fichier /etc/docker/daemon.json :

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}
Bonne pratique : en production sérieuse, envoyez vos logs vers un système centralisé (Loki, ELK, Datadog). Les logs locaux restent utiles pour le debug immédiat, mais ne doivent pas être votre source de vérité.

Conclusion

Ces sept erreurs ne sont pas des cas de figure théoriques. Ce sont des problèmes que je rencontre régulièrement, y compris dans des équipes expérimentées. Docker simplifie le déploiement, mais il n'élimine pas la complexité -- il la déplace.

Pour récapituler les bonnes pratiques essentielles :

  • Exécutez vos conteneurs avec un utilisateur non-root
  • Taguez vos images avec des versions explicites
  • Implémentez des healthchecks qui testent réellement l'état de l'application
  • Utilisez un gestionnaire de secrets, pas des variables d'environnement
  • Gérez les permissions des volumes avec des UID/GID fixes
  • Définissez des limites de CPU et mémoire pour chaque conteneur
  • Configurez la rotation des logs dès le premier déploiement

Aucune de ces corrections n'est complexe individuellement. C'est leur application systématique qui fait la différence entre un environnement Docker fragile et une infrastructure de production fiable. Si vous débutez avec Docker, mon tutoriel Docker couvre les fondamentaux avant d'aborder ces sujets avancés.

Cet article vous a plu ?

Commentaires

Morgann Riu
Morgann Riu

Expert en cybersécurité et administration Linux. J'aide les entreprises à sécuriser et optimiser leurs infrastructures critiques.

Retour au blog

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.