Un fichier docker-compose.yml qui tourne sans erreur n’est pas un fichier sécurisé. C’est la confusion la plus répandue chez les équipes qui passent d’un environnement de développement à un serveur de production : le stack démarre, les conteneurs répondent, et personne ne vérifie que l’utilisateur tourne en root, que les secrets traînent dans les variables d’environnement, ou que l’image utilise encore le tag latest. Avec Docker Engine 29.8.1 (publié le 15 septembre 2026) et Docker Compose 5.5.1 (3 septembre 2026), les outils pour corriger ça existent depuis longtemps. Le problème, ce n’est pas l’outillage, c’est l’absence de méthode.

Ce tutoriel part d’un fichier Compose ordinaire et le transforme, étape par étape, en une stack durcie pour la production : images épinglées par digest, secrets isolés, système de fichiers en lecture seule, privilèges Linux réduits au minimum, réseau segmenté et ressources plafonnées. Douze étapes, un projet complet à la fin, et une liste de pièges qui reviennent sans cesse dans les audits de configuration Docker.

Ce guide s’adresse aux équipes qui gèrent une ou plusieurs applications sur un hôte unique ou un petit parc de serveurs, un scénario courant pour les PME françaises, les startups et les projets internes qui n’ont ni l’échelle ni le budget pour justifier un cluster Kubernetes complet. Chaque étape est indépendante et peut s’appliquer séparément : vous n’avez pas besoin de tout refaire d’un coup pour obtenir un gain de sécurité mesurable. Certaines, comme le retrait des capacités Linux, prennent moins de dix minutes à mettre en place sur une stack existante.

Pourquoi la sécurité de Docker Compose ne doit plus être une réflexion après coup

Docker Compose reste l’outil de déploiement le plus utilisé pour les applications conteneurisées de petite et moyenne taille, loin devant Kubernetes en nombre de stacks actives selon les téléchargements du plugin officiel sur le dépôt docker/compose. Sa simplicité est aussi son piège : un fichier YAML de vingt lignes suffit à lancer une base de données, une API et un frontend, et rien dans cette simplicité n’oblige à penser sécurité. Contrairement à Kubernetes, qui impose des objets RBAC et des NetworkPolicy dès le départ, Compose laisse tout ouvert par défaut.

Les CVE publiées en 2025 sur l’écosystème Docker rappellent que la surface d’attaque ne se limite pas à l’image applicative. Le runtime bas niveau runc a reçu trois correctifs en 2025 (CVE-2025-31133, CVE-2025-52565, CVE-2025-52881) pour des contournements de protections sur /proc pouvant permettre une sortie de conteneur, intégrés dans Docker Engine 28 selon les notes de version officielles. Docker Compose lui-même a corrigé en 2025 une faille de traversée de chemin (CVE-2025-62725) lors de la résolution d’artefacts OCI distants, capable d’écraser des fichiers locaux hors du répertoire de cache prévu. Aucune de ces failles n’exige une configuration exotique pour être exploitée : elles touchent des installations Docker standard.

Le raisonnement “je suis derrière un pare-feu” ne tient pas non plus. Un conteneur compromis via une dépendance applicative vulnérable (une bibliothèque Node.js, un plugin WordPress, une image de base obsolète) donne accès à tout ce que ce conteneur peut voir : le socket Docker s’il est monté par erreur, les autres conteneurs sur le même réseau, les secrets s’ils sont exposés en variable d’environnement. La sécurisation de Compose consiste précisément à réduire ce que chaque conteneur peut atteindre une fois compromis, et non à empêcher toute compromission, un objectif irréaliste.

Ce principe porte un nom en sécurité informatique : la défense en profondeur. Aucune des douze étapes de ce tutoriel n’est, prise isolément, suffisante pour arrêter un attaquant déterminé. Un réseau segmenté n’empêche pas une fuite de secret. Un secret bien géré n’empêche pas une élévation de privilèges via une capacité Linux oubliée. C’est l’empilement de ces couches qui rend l’exploitation coûteuse : chaque contrôle supplémentaire oblige l’attaquant à trouver une faille différente pour progresser, et la plupart abandonnent avant d’y arriver sur une cible qui ne rapporte pas assez pour l’effort investi.

Prérequis : versions et outils nécessaires

Ce tutoriel a été vérifié avec les versions suivantes, actuelles au 29 septembre 2026. Avant de commencer, confirmez vos versions locales avec docker version et docker compose version. Les commandes de ce guide supposent le nouveau format Compose Specification, sans champ version: obligatoire en tête de fichier.

ComposantVersion utiliséeRemarque
Docker Engine29.8.1Publiée le 15 septembre 2026, branche stable
Docker Compose (plugin CLI)5.5.1Publiée le 3 septembre 2026, dépôt docker/compose
Système d’exploitationUbuntu 24.04 LTS ou Debian 12Kernel 5.15 ou supérieur recommandé pour seccomp et cgroups v2
Trivydernière version stableVérifiez la version exacte dans votre CI avant de l’épingler
Docker Scoutintégré au CLI DockerCommande docker scout cves
Accèssudo ou rootNécessaire pour modifier le démon et les groupes système

Vous aurez aussi besoin d’un accès en écriture à un registre d’images (Docker Hub, GitHub Container Registry ou un registre privé) pour récupérer les digests SHA256, et d’un reverse proxy comme Traefik ou nginx pour l’étape TLS. Aucun de ces outils n’est exotique : l’objectif de ce tutoriel est de combiner des fonctionnalités déjà présentes dans Compose, pas d’ajouter des dépendances lourdes.

Prévoyez aussi un environnement de test qui reproduit fidèlement la production, ne serait-ce qu’une machine virtuelle avec les mêmes contraintes de ressources. Chaque étape de ce tutoriel doit être validée hors production avant d’être appliquée sur une stack qui sert de vrais utilisateurs, en particulier les étapes qui touchent au système de fichiers et aux privilèges, les plus susceptibles de casser une application mal préparée à ces contraintes.

Étape 1 : auditer votre fichier Compose actuel

Avant de changer quoi que ce soit, générez la configuration résolue de votre stack. Cette commande fusionne tous vos fichiers Compose et override, résout les variables d’environnement, et affiche exactement ce que Docker va exécuter.

docker compose config

# Pour repérer les secrets qui fuitent en clair dans les variables résolues
docker compose config --no-interpolate

# Lister les images utilisées et leurs tags actuels
docker compose config --images

Passez ce résultat au crible avec trois questions simples : est-ce qu’un service utilise privileged: true ? Est-ce qu’un mot de passe apparaît en clair dans environment: ? Est-ce qu’une image pointe vers latest plutôt que vers un tag ou un digest précis ? Sur la plupart des stacks qui n’ont jamais été auditées, les trois réponses sont oui. C’est le point de départ normal, pas une raison de tout réécrire d’un coup.

Étape 2 : séparer les fichiers pour la production

Ne modifiez jamais directement le fichier de développement. Compose accepte plusieurs fichiers combinés, ce qui permet de garder une base commune et d’ajouter les réglages de production dans un fichier séparé, jamais commité avec des secrets en clair.

# Structure recommandée
# docker-compose.yml        -> base commune (images, réseaux, volumes)
# docker-compose.prod.yml   -> réglages de production (limites, secrets, restart policy)
# .env                      -> variables non sensibles uniquement

docker compose -f docker-compose.yml -f docker-compose.prod.yml config

# Déploiement effectif
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Ajoutez docker-compose.prod.yml et tout fichier .env contenant des valeurs sensibles à votre .gitignore. Un fichier .env n’est pas un coffre-fort : c’est une commodité pour la configuration non sensible en développement. En production, les vrais secrets passent par le mécanisme dédié que vous configurerez à l’étape 5.

Étape 3 : épingler les images par digest et bannir latest

Un tag comme postgres:16 peut pointer vers une image différente d’un jour à l’autre si le mainteneur republie le tag. Un digest SHA256, lui, référence un contenu d’image exact et immuable. C’est la différence entre redéployer la même chose et redéployer ce que le registre donne aujourd’hui.

# Récupérer le digest d'une image déjà tirée
docker inspect --format='{{index .RepoDigests 0}}' postgres:16

# Sortie type
# postgres@sha256:8f1f6c3f2e9a4b6d7c5e3f1a2b4c6d8e0f2a4c6e8f0a2b4c6d8e0f2a4c6d8e0f

# Utilisation dans le fichier Compose
# image: postgres@sha256:8f1f6c3f2e9a4b6d7c5e3f1a2b4c6d8e0f2a4c6e8f0a2b4c6d8e0f2a4c6d8e0f

En pratique, gardez une trace lisible du tag à côté du digest en commentaire (# postgres:16.4) pour que l’équipe sache ce que représente le digest sans avoir à le décoder. Automatisez la mise à jour des digests dans votre pipeline CI plutôt que de les copier-coller à la main, sinon vous figez vos images indéfiniment et manquez les correctifs de sécurité.

Si vos images proviennent d’un registre privé, configurez l’authentification via docker login côté CI et non via des identifiants stockés en clair dans le fichier Compose. La plupart des plateformes CI (GitHub Actions, GitLab CI) proposent un mécanisme de secrets natif pour ce type d’identifiants, à privilégier systématiquement plutôt qu’une variable d’environnement inscrite dans le pipeline lui-même.

Étape 4 : scanner les images avec Trivy et Docker Scout

Épingler une image ne garantit pas qu’elle est saine, seulement qu’elle est stable. Le scan de vulnérabilités reste une étape séparée, à exécuter au build et avant chaque déploiement.

# Scanner une image avec Trivy
trivy image --severity HIGH,CRITICAL postgres@sha256:8f1f6c3f...

# Scanner avec Docker Scout
docker scout cves postgres@sha256:8f1f6c3f...

# Générer un SBOM pour archivage et audit
trivy image --format cyclonedx --output sbom.json postgres@sha256:8f1f6c3f...

Scannez l’image finale de votre application, pas seulement l’image de base : une dépendance ajoutée dans votre Dockerfile peut introduire une vulnérabilité absente de l’image parente. Conservez le rapport de scan avec chaque déploiement, il devient une preuve utile en cas d’audit de conformité ou d’incident. L’absence de vulnérabilité détectée à l’instant T ne signifie pas que la configuration est sûre pour autant : le scan couvre les paquets connus, pas les erreurs de configuration Compose que vous corrigez dans les étapes suivantes.

Étape 5 : gérer les secrets sans les exposer

Les secrets Compose se déclarent au niveau supérieur du fichier et sont montés comme fichiers en lecture seule sous /run/secrets/ à l’intérieur du conteneur, jamais comme variable d’environnement visible via docker inspect.

services:
  api:
    image: monapp/api@sha256:...
    secrets:
      - db_password
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password

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

Deux points comptent particulièrement ici. D’abord, déclarer un secret au niveau supérieur ne lui donne pas automatiquement accès à tous les services : chaque service doit le lister explicitement dans sa propre section secrets:. Ensuite, ne confondez jamais l’interpolation Compose (${DB_PASSWORD}) avec un vrai mécanisme de secret. L’interpolation injecte la valeur en clair dans la configuration résolue, visible par docker compose config et potentiellement dans vos journaux de déploiement. En production, préférez un gestionnaire de secrets externe (Vault, un coffre-fort cloud, ou les secrets Swarm si vous orchestrez avec plusieurs nœuds) avec rotation et contrôle d’accès, et injectez-le via ce mécanisme secrets: plutôt que via des variables classiques.

Étape 6 : verrouiller le système de fichiers en lecture seule

Un conteneur compromis qui ne peut rien écrire sur son propre système de fichiers perd une bonne partie de son utilité pour un attaquant : pas de persistance de malware, pas de modification de binaires, pas d’installation d’outils additionnels.

services:
  api:
    image: monapp/api@sha256:...
    read_only: true
    tmpfs:
      - /tmp
      - /run

Le champ read_only: true rend l’ensemble du système de fichiers du conteneur immuable. Ajoutez des montages tmpfs uniquement pour les répertoires que l’application doit réellement écrire de façon temporaire, et des volumes nommés pour les données qui doivent persister entre redémarrages. Testez systématiquement l’application après ce changement : beaucoup d’images écrivent silencieusement dans /var/cache, leur répertoire de travail ou un dossier de logs local, et plantent au démarrage sans message clair une fois le système de fichiers verrouillé.

Étape 7 : réduire les privilèges Linux

Par défaut, un conteneur Docker démarre avec un sous-ensemble déjà réduit de capacités Linux, mais ce sous-ensemble reste plus large que ce dont la plupart des applications ont réellement besoin. La bonne pratique consiste à tout retirer, puis à ne réintroduire que le strict nécessaire.

services:
  reverse-proxy:
    image: nginx@sha256:...
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    security_opt:
      - no-new-privileges:true

Ici, NET_BIND_SERVICE est réintroduite uniquement parce que nginx doit se lier au port 443, un privilège normalement réservé à root sur Linux. L’option no-new-privileges:true empêche un processus de gagner des privilèges supplémentaires via des binaires set-user-ID, même si l’exécutable en contient. Elle ne remplace ni le retrait de capacités ni un profil seccomp, elle s’ajoute en complément. Évitez privileged: true sauf besoin exceptionnel documenté (accès matériel direct, par exemple) : cette option annule quasiment toute l’isolation apportée par les espaces de noms Linux.

Étape 8 : exécuter les conteneurs en utilisateur non-root

Beaucoup d’images officielles tournent encore en root par défaut, souvent par commodité de packaging plutôt que par nécessité technique.

services:
  api:
    image: monapp/api@sha256:...
    user: "10001:10001"

Le champ user: remplace l’utilisateur défini par défaut dans l’image. Choisissez un UID qui n’existe pas déjà sur l’hôte pour éviter toute collision de permissions sur les volumes montés. Pour aller plus loin, activez le remappage d’espace de noms utilisateur (userns-remap) au niveau du démon Docker : il fait correspondre le root du conteneur à un UID non privilégié sur l’hôte, ce qui limite fortement l’impact d’une éventuelle sortie de conteneur. Vérifiez avant de l’activer que vos volumes en bind mount et vos permissions de fichiers restent cohérents. Ce réglage casse parfois des stacks qui supposaient un accès root implicite aux fichiers montés.

Étape 9 : isoler et segmenter le réseau

Par défaut, tous les services d’un même fichier Compose partagent un réseau commun et peuvent se joindre entre eux par leur nom de service. C’est pratique en développement, dangereux en production si un service exposé publiquement peut atteindre directement votre base de données.

services:
  frontend:
    networks: [public]
  api:
    networks: [public, backend]
  db:
    networks: [backend]

networks:
  public:
  backend:
    internal: true

Le réseau backend marqué internal: true n’a aucune route vers l’extérieur : les conteneurs qui y sont connectés peuvent parler entre eux, mais aucun trafic sortant vers internet n’est possible depuis ce réseau, et rien depuis internet ne peut l’atteindre directement. Seul le service api, présent sur les deux réseaux, fait le pont entre la partie publique et la base de données. Un attaquant qui compromet le frontend ne peut pas, depuis ce point d’entrée, ouvrir une connexion directe vers la base.

Cette segmentation a un coût opérationnel réel à anticiper : les outils de diagnostic classiques (un client SQL lancé depuis votre poste, par exemple) ne peuvent plus atteindre directement la base une fois le réseau backend isolé. Prévoyez un conteneur d’administration temporaire, lancé ponctuellement sur le même réseau interne avec docker compose run, plutôt que de rouvrir le réseau en permanence pour le confort du débogage.

Étape 10 : plafonner les ressources CPU et mémoire

Un conteneur sans limite de ressources peut, par un bug applicatif ou une attaque par déni de service, consommer toute la mémoire ou tout le CPU de l’hôte et affecter les autres services de la même machine.

services:
  api:
    image: monapp/api@sha256:...
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Les limites de deploy.resources fonctionnent avec docker compose up depuis les versions récentes du CLI, pas seulement en mode Swarm. Ajoutez toujours une rotation de logs (max-size, max-file) : sans elle, un service bavard ou compromis peut remplir le disque de l’hôte en quelques heures et provoquer une panne bien plus large que le problème initial.

Étape 11 : configurer healthchecks et politique de redémarrage

Un conteneur qui plante silencieusement et redémarre en boucle sans jamais devenir sain masque souvent un problème de sécurité, pas seulement un bug : mauvaise permission après le passage en lecture seule, secret manquant, capacité retirée trop tôt.

services:
  api:
    image: monapp/api@sha256:...
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    restart: unless-stopped

Préférez unless-stopped à always dans la plupart des cas : la politique always relance même un conteneur que vous avez arrêté volontairement après un redémarrage de l’hôte, ce qui complique le débogage d’un incident en cours. Le start_period évite qu’un conteneur lent à démarrer soit déclaré en échec avant même d’avoir eu le temps de s’initialiser.

Étape 12 : ajouter un reverse proxy TLS et valider avant déploiement

Aucun service applicatif ne devrait être exposé directement à internet sans passer par un reverse proxy qui gère TLS, les en-têtes de sécurité et, idéalement, la limitation de débit.

services:
  traefik:
    image: traefik@sha256:...
    ports:
      - "443:443"
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - certs:/certs
    networks: [public]

# Validation finale avant déploiement en CI/CD
docker compose -f docker-compose.yml -f docker-compose.prod.yml config --quiet
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --remove-orphans

Le montage du fichier de configuration Traefik en :ro (lecture seule) empêche un conteneur compromis de modifier sa propre configuration réseau. Intégrez la commande docker compose config --quiet dans votre pipeline CI/CD comme étape de validation syntaxique avant tout déploiement : elle échoue si le fichier fusionné contient une erreur, avant que celle-ci n’atteigne la production. L’option --remove-orphans nettoie les conteneurs de services supprimés du fichier Compose, souvent oubliés et laissés tournants avec une configuration obsolète.

Migrer une stack existante sans interruption de service

Appliquer les douze étapes d’un coup sur une stack qui tourne déjà en production est le meilleur moyen de provoquer une panne. Procédez service par service, en commençant par le moins critique, et validez chaque changement avant de passer au suivant.

# 1. Appliquer un changement sur un seul service à la fois
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps api

# 2. Vérifier l'état de santé avant de continuer
docker compose ps api
docker compose logs --tail=50 api

# 3. En cas d'échec, revenir en arrière sur ce service uniquement
docker compose -f docker-compose.yml up -d --no-deps api

L’option --no-deps évite que Docker ne redémarre aussi les services dépendants, ce qui limite le rayon d’impact d’un changement raté. Commencez par les réglages les plus sûrs (limites de ressources, healthchecks, rotation de logs) avant de passer aux changements qui peuvent casser une application mal préparée (système de fichiers en lecture seule, retrait de capacités, utilisateur non-root). Gardez une fenêtre de maintenance annoncée pour les deux ou trois derniers changements les plus risqués, même si l’objectif reste un déploiement sans interruption pour la majorité des étapes.

Le projet complet : une stack Docker Compose prête pour la production

Voici l’assemblage des douze étapes dans un seul fichier de production, à combiner avec votre fichier de base. Adaptez les noms de services, les images et les digests à votre application.

services:
  traefik:
    image: traefik@sha256:REMPLACER_PAR_DIGEST
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    cap_add: [NET_BIND_SERVICE]
    security_opt: [no-new-privileges:true]
    ports: ["443:443"]
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - certs:/certs
    networks: [public]
    restart: unless-stopped
    deploy:
      resources:
        limits: {cpus: "0.5", memory: 256M}
    logging:
      options: {max-size: "10m", max-file: "3"}

  api:
    image: monapp/api@sha256:REMPLACER_PAR_DIGEST
    read_only: true
    tmpfs: [/tmp]
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    user: "10001:10001"
    networks: [public, backend]
    secrets: [db_password]
    environment:
      - DB_PASSWORD_FILE=/run/secrets/db_password
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    deploy:
      resources:
        limits: {cpus: "1.0", memory: 512M}

  db:
    image: postgres@sha256:REMPLACER_PAR_DIGEST
    read_only: true
    tmpfs: [/tmp, /run]
    cap_drop: [ALL]
    security_opt: [no-new-privileges:true]
    networks: [backend]
    secrets: [db_password]
    volumes:
      - db_data:/var/lib/postgresql/data
    restart: unless-stopped
    deploy:
      resources:
        limits: {cpus: "1.0", memory: 1G}

networks:
  public:
  backend:
    internal: true

volumes:
  certs:
  db_data:

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

Ce squelette couvre l’essentiel d’une stack de production : réseau segmenté, secrets isolés du code, système de fichiers verrouillé, privilèges réduits au minimum et ressources plafonnées. Ajoutez ensuite votre supervision (Prometheus, un agent de logs centralisés) et votre stratégie de sauvegarde du volume db_data selon les besoins réels de votre organisation.

Vulnérabilités récentes à connaître

Épingler des versions récentes ne suffit pas si vous ignorez pourquoi elles sont récentes. Voici les correctifs 2025 les plus pertinents pour une stack Compose en production.

CVEComposantImpactVersion corrigée
CVE-2025-62725Docker ComposeTraversée de chemin lors de la résolution d’artefacts OCI distants, écrasement de fichiers locaux hors du cacheCompose 2.40.2 et suivantes
CVE-2025-31133runcContournement des protections d’écriture sur /proc, sortie de conteneur possiblerunc 1.2.8 / 1.3.3 / 1.4.0-rc.3
CVE-2025-52565runcSortie de conteneur via bypass des protections /procrunc 1.2.8 / 1.3.3 / 1.4.0-rc.3
CVE-2025-52881runcÉcriture arbitraire dans /proc, sortie de conteneurrunc 1.2.8 / 1.3.3 / 1.4.0-rc.3
CVE-2025-54388Docker Engine (Moby)Ports publiés en loopback redevenaient accessibles depuis le réseau local après un rechargement de firewalldCorrectif intégré à Docker Engine 28.x

Les trois failles runc illustrent bien pourquoi le retrait de capacités et le système de fichiers en lecture seule des étapes 6 et 7 ne sont pas des options facultatives : même un runtime à jour peut avoir un correctif en retard de quelques semaines sur la découverte d’une faille, et une isolation en profondeur limite les dégâts pendant cette fenêtre. Consultez les notes de version d’Engine 29 sur docs.docker.com avant chaque montée de version pour vérifier les correctifs de sécurité inclus.

Docker Compose face à Swarm et Kubernetes sur le plan de la sécurité

Une question revient souvent une fois une stack Compose durcie : est-ce suffisant, ou faut-il migrer vers un orchestrateur plus complet ? La réponse dépend moins de la sécurité en elle-même que du nombre de machines à gérer. Compose applique ses contrôles (secrets, capacités, réseau) sur un seul hôte, sans mécanisme natif de bascule automatique si ce hôte tombe. Swarm ajoute une couche de PKI intégrée et une distribution automatique des secrets chiffrés entre plusieurs nœuds. Kubernetes va plus loin avec des objets RBAC natifs, des NetworkPolicy déclaratives et des admission controllers capables de rejeter un déploiement qui ne respecte pas une politique de sécurité définie à l’avance.

CritèreDocker ComposeDocker SwarmKubernetes
Nombre d’hôtes géréUn seul, en pratiquePlusieurs, cluster natifPlusieurs, cluster natif
Secrets chiffrés en transit entre nœudsNon applicable (un seul hôte)Oui, via TLS mutuel intégréOui, avec chiffrement au repos à activer séparément
Politiques réseau déclarativesRéseaux internes manuelsRéseaux overlay chiffrablesNetworkPolicy natives
Contrôle d’admission des déploiementsAucunLimitéAdmission controllers (OPA, Kyverno)
Complexité opérationnelleFaibleModéréeÉlevée

Pour une stack sur un hôte unique, les douze étapes de ce tutoriel couvrent l’essentiel des risques pratiques. La migration vers Swarm ou Kubernetes se justifie surtout par un besoin de haute disponibilité multi-nœuds ou de gouvernance à l’échelle d’une organisation, pas uniquement par un objectif de sécurité que Compose ne pourrait pas atteindre seul.

Pièges courants à éviter

Ces erreurs reviennent dans presque toutes les stacks Compose non auditées. Elles sont simples à corriger une fois identifiées, mais faciles à manquer parce qu’elles ne causent aucune erreur visible au démarrage.

  • Monter le socket Docker dans un conteneur applicatif. Un montage /var/run/docker.sock:/var/run/docker.sock donne au conteneur un contrôle total sur l’hôte, équivalent à un accès root. Réservez ce montage aux outils d’administration explicitement conçus pour, jamais à une API métier.
  • Confondre variable d’environnement et secret. docker inspect affiche toutes les variables d’environnement en clair, y compris pour un utilisateur qui n’a pas de shell dans le conteneur mais accès au démon Docker.
  • Exposer des ports inutiles sur 0.0.0.0. La syntaxe ports: ["5432:5432"] lie le port sur toutes les interfaces réseau de l’hôte. Pour un service interne, utilisez expose: à la place, ou liez explicitement sur l’interface loopback avec "127.0.0.1:5432:5432".
  • Oublier de retester après le passage en lecture seule. Certaines images écrivent des fichiers de verrouillage ou de cache dans des chemins inattendus. L’application semble démarrer puis échoue au premier traitement réel.
  • Garder docker-compose.override.yml avec des réglages de développement en production. Compose charge automatiquement ce fichier s’il existe dans le répertoire, ce qui peut réintroduire silencieusement des ports de debug ou des volumes bind mount vers le code source.
  • Ignorer les avertissements de docker compose config. Un avertissement sur une variable non définie ou un champ obsolète indique souvent une divergence entre ce que vous pensez déployer et ce qui sera réellement exécuté.

Dépannage : les problèmes les plus fréquents

Voici les symptômes les plus courants après un durcissement de stack Compose, avec leur cause habituelle et la correction associée.

SymptômeCause probableSolution
Le conteneur redémarre en boucle après read_only: trueL’application écrit dans un répertoire non monté en tmpfs ou en volumeIdentifier le chemin avec docker logs, ajouter un tmpfs ou un volume dédié
“Permission denied” au démarrage après ajout de user:Le volume monté appartient à root et l’UID non-root ne peut pas y écrireAjuster les permissions du volume avec chown ou utiliser un volume géré par Docker
Le service ne peut plus se lier au port 443cap_drop: [ALL] a retiré NET_BIND_SERVICERéajouter explicitement cap_add: [NET_BIND_SERVICE]
Deux conteneurs sur des réseaux différents ne se joignent plusSegmentation réseau de l’étape 9 appliquée trop strictementVérifier que le service intermédiaire (souvent l’API) est bien présent sur les deux réseaux
docker compose config échoue en CI mais fonctionne en localUne variable d’environnement définie localement n’existe pas dans le pipelineDéclarer toutes les variables requises dans les secrets ou variables du pipeline CI
Le conteneur est tué (OOMKilled) peu après démarrageLa limite mémoire de l’étape 10 est trop basse pour la charge réelleMesurer la consommation réelle avec docker stats avant de fixer la limite définitive
Le secret n’apparaît pas dans /run/secrets/Le service ne liste pas le secret dans sa propre section secrets:Ajouter explicitement le nom du secret sous le service concerné, pas seulement au niveau supérieur
Le healthcheck échoue alors que l’application répondL’outil utilisé dans le test: (curl, wget) a été retiré de l’image de base allégéeUtiliser un outil présent dans l’image finale ou ajouter un binaire de test minimal dans le build

Conseils avancés pour aller plus loin

Une fois les douze étapes en place, plusieurs réglages supplémentaires renforcent encore la posture de sécurité sans bouleverser l’architecture existante. Un profil seccomp personnalisé, plus restrictif que le profil par défaut de Docker, réduit l’ensemble des appels système autorisés à ceux réellement utilisés par votre application. Générez-en un avec docker run --security-opt seccomp=audit.json en mode audit, observez les appels système effectivement utilisés en conditions réelles, puis construisez un profil restrictif à partir de cette observation plutôt que de partir d’une liste théorique.

Pour les stacks qui gèrent des données sensibles, activez le remappage d’espace de noms utilisateur au niveau du démon (userns-remap dans /etc/docker/daemon.json), déjà mentionné à l’étape 8, et combinez-le avec AppArmor ou SELinux si votre distribution le supporte. La documentation officielle Docker sur l’exécution sans root (docs.docker.com) détaille aussi le mode rootless complet, qui va plus loin en faisant tourner le démon Docker lui-même sans privilèges root, une option pertinente pour les environnements multi-utilisateurs partagés.

Enfin, traitez le fichier Compose lui-même comme une entrée non fiable s’il provient d’un dépôt tiers ou d’un artefact OCI distant. La documentation sur le modèle de confiance de Compose (docs.docker.com) recommande explicitement de ne jamais exécuter un fichier Compose sans l’avoir inspecté, exactement comme on ne lancerait pas un script shell téléchargé sans le lire. Automatisez cette inspection dans votre pipeline avec un linter YAML et un scan de secrets (comme gitleaks) avant tout docker compose up en production.

Journalisez enfin les événements Docker eux-mêmes, pas seulement les logs applicatifs. La commande docker events expose en temps réel les créations, arrêts et modifications de conteneurs sur l’hôte, un flux précieux pour détecter un comportement anormal comme le démarrage d’un conteneur privilégié en dehors de votre pipeline de déploiement habituel. Redirigez ce flux vers votre système de logs centralisé plutôt que de le laisser disparaître dans le terminal d’un administrateur, il devient alors une source d’alerte exploitable en cas d’incident.

Foire aux questions

Docker Compose est-il adapté à la production ou seulement au développement ?

Compose convient bien à la production pour des déploiements sur un seul hôte ou un petit nombre de machines. Au-delà, pour de l’orchestration multi-nœuds avec bascule automatique, Docker Swarm ou Kubernetes prennent le relais, mais les mêmes principes de durcissement présentés ici (secrets, capacités, réseau segmenté) s’appliquent aussi à ces plateformes.

Quelle est la différence entre secrets: et une variable d’environnement classique ?

Un secret Compose est monté comme fichier en lecture seule sous /run/secrets/ et n’apparaît pas dans la sortie de docker inspect. Une variable d’environnement classique est visible en clair par quiconque a accès au démon Docker, y compris dans les journaux de certains outils de supervision.

Faut-il toujours utiliser un digest SHA256 plutôt qu’un tag ?

En production, oui pour les images critiques (bases de données, images d’authentification). Le digest garantit que le contenu déployé aujourd’hui est identique à celui testé en préproduction. Gardez toutefois une automatisation pour faire évoluer ces digests régulièrement, sinon vous manquez les correctifs de sécurité en amont.

read_only: true casse-t-il forcément les applications existantes ?

Pas forcément, mais il faut le tester. La plupart des applications web modernes n’écrivent que dans des répertoires temporaires ou de cache identifiables. Ajoutez un tmpfs pour ces répertoires plutôt que d’abandonner le système de fichiers en lecture seule au premier problème rencontré.

Docker Scout et Trivy font-ils double emploi ?

Ils se recoupent partiellement mais utilisent des bases de vulnérabilités et des heuristiques différentes. Beaucoup d’équipes exécutent les deux en CI et traitent toute vulnérabilité critique signalée par l’un ou l’autre outil, plutôt que de se fier à une seule source. Documentez les faux positifs identifiés au fil du temps pour éviter de réanalyser le même cas à chaque exécution du pipeline.

Le remappage d’espace de noms utilisateur (userns-remap) est-il obligatoire ?

Non, mais il est recommandé pour les environnements exposés ou multi-locataires. Il ajoute une couche d’isolation supplémentaire en cas de sortie de conteneur, au prix d’une compatibilité à vérifier avec vos volumes en bind mount et certaines images qui supposent un accès root direct aux fichiers hôtes.

Comment savoir si mon fichier Compose actuel contient déjà des secrets en clair ?

Exécutez docker compose config --no-interpolate et cherchez manuellement les motifs suspects (mots de passe, clés API, chaînes de connexion), ou automatisez la détection avec un outil de scan de secrets comme gitleaks ou trufflehog intégré à votre pipeline CI.

Ce durcissement ralentit-il les performances de la stack ?

L’impact mesuré est marginal pour la grande majorité des réglages (capacités, lecture seule, secrets). Les limites de ressources de l’étape 10 peuvent en revanche dégrader les performances si elles sont fixées trop bas : mesurez la consommation réelle avec docker stats avant de figer des valeurs définitives plutôt que de deviner.