Aller plus loin

Sécuriser ses conteneurs Docker : le guide pratique

Mis à jour le 4 octobre 2026 · 11 min de lecture

Un conteneur n’est pas une machine virtuelle. Il partage le noyau du serveur, et par défaut il tourne en root. Si une application a une faille, l’attaquant entre dans le conteneur avec des droits qui, mal réglés, peuvent mener au serveur entier.

L’objectif de ce guide : qu’une application piratée reste coincée dans sa boîte, sans accès aux autres applications ni au serveur. Ce sont les règles que j’applique à chaque service sur mes serveurs.

Prérequis : avoir déjà sécurisé le serveur lui-même (sécuriser son VPS).

1. Un utilisateur dédié par application

Créez un utilisateur système sans connexion possible, propriétaire des seules données de l’application :

sudo useradd --system -u 2001 -U -M -d /nonexistent -s /usr/sbin/nologin svc-vaultwarden
getent group svc-vaultwarden          # vérifiez que le groupe a bien le même numéro (2001)
sudo chown -R 2001:2001 /opt/stacks/vaultwarden/data
sudo chmod 700 /opt/stacks/vaultwarden/data

Le piège : useradd -u 2001 -U fixe le numéro de l’utilisateur, mais le groupe reçoit le premier numéro libre, souvent différent (982, par exemple). Le conteneur, lancé en 2001:2001, ne peut alors plus lire ses fichiers. Vérifiez avec getent group, et corrigez avec sudo groupmod -g 2001 svc-vaultwarden.

Puis, dans le compose.yml :

services:
  vaultwarden:
    user: "2001:2001"

Certaines images doivent démarrer en root pour préparer leurs dossiers, puis changent d’utilisateur toutes seules. Elles se règlent alors avec des variables, souvent PUID et PGID : lisez leur documentation.

2. Retirer les privilèges inutiles

    security_opt:
      - no-new-privileges:true   # impossible de regagner des droits (sudo, setuid…)
    cap_drop:
      - ALL                      # retire toutes les « capacités » root du noyau

Les capacités sont des morceaux des pouvoirs de root : changer le propriétaire d’un fichier (CHOWN), ouvrir un port sous 1024 (NET_BIND_SERVICE)… On les retire toutes, puis on rajoute uniquement celles qui manquent, si l’application refuse de démarrer :

    cap_add:
      - CHOWN
      - SETUID
      - SETGID

Lisez les journaux (docker compose logs) : un message « operation not permitted » indique en général la capacité qui manque.

3. Un système de fichiers en lecture seule

    read_only: true
    tmpfs:
      - /tmp
    volumes:
      - ./data:/data

Le conteneur ne peut alors écrire que dans ses volumes et dans /tmp (en mémoire). Un attaquant ne peut ni modifier le programme, ni déposer un outil ailleurs. C’est très efficace pour les sites statiques et beaucoup de petites applications.

4. Des réseaux séparés

Par défaut, chaque projet docker-compose a son propre réseau : c’est déjà bien. Allez plus loin pour les bases de données, qui n’ont aucune raison de parler à Internet :

services:
  app:
    networks: [web, interne]
  db:
    networks: [interne]

networks:
  web:
    external: true      # le réseau partagé avec le reverse proxy
  interne:
    internal: true      # aucun accès à Internet, aucun accès depuis l'extérieur

La base n’est joignable que par l’application. Et surtout, jamais de ports: sur une base de données.

5. Publier les ports en local seulement

Rappel du piège le plus courant : Docker contourne le pare-feu UFW. Un ports: "8080:80" ouvre le port 8080 à tout Internet, même si UFW dit le contraire.

    ports:
      - "127.0.0.1:8080:80"   # seul le serveur lui-même (le reverse proxy) y accède

Avec Traefik sur le même réseau Docker, vous n’avez même pas besoin de ports: du tout.

6. Ranger les secrets à part

Les mots de passe ne vont jamais dans le compose.yml (qui finit souvent dans git) :

echo "DB_PASSWORD=$(openssl rand -hex 24)" | sudo tee .env > /dev/null
sudo chmod 600 .env
    env_file: .env

Ajoutez .env à votre .gitignore. Et générez les mots de passe : jamais de mot de passe réutilisé ou choisi « à la main ».

7. Ne jamais donner le socket Docker

Monter /var/run/docker.sock dans un conteneur lui donne le contrôle total du serveur : il peut lancer un conteneur privilégié et lire n’importe quel fichier. Traefik, Portainer ou Watchtower le demandent souvent.

Placez un socket-proxy entre les deux, qui ne laisse passer que les lectures nécessaires :

  socket-proxy:
    image: tecnativa/docker-socket-proxy:latest
    environment:
      CONTAINERS: 1     # lire la liste des conteneurs : c'est tout ce dont Traefik a besoin
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks: [socket]

  traefik:
    # ...
    command:
      - --providers.docker.endpoint=tcp://socket-proxy:2375
    networks: [web, socket]

networks:
  socket:
    internal: true

Pour les outils qui démarrent des conteneurs (certains proxys qui démarrent un service à la demande, par exemple), demandez-vous si le confort vaut ce risque. Souvent, une solution sans socket existe.

8. Limiter les ressources

Une application qui s’emballe (fuite de mémoire, attaque) ne doit pas faire tomber les autres :

    deploy:
      resources:
        limits:
          memory: 512M
          cpus: "1.0"
          pids: 200

Choisissez des limites confortables : regardez la consommation réelle avec docker stats, puis ajoutez de la marge.

9. Des mises à jour régulières, mais pas immédiates

Une image non mise à jour accumule les failles connues. Mais une mise à jour installée le jour de sa sortie peut contenir un bug, ou, plus rarement, avoir été compromise. Le bon compromis :

  • épingler les images sur une version majeure (postgres:17-alpine, nextcloud:34-apache) plutôt que latest ;
  • laisser passer quelques jours (une à deux semaines) avant d’installer une nouvelle version ;
  • lire les notes de version des applications sensibles avant de mettre à jour.

Méthode détaillée : mettre à jour ses applications Docker sans rien casser.

10. Vérifier le résultat

Faites confiance, mais vérifiez :

# Qui tourne en root ? (vide ou « 0 » = root)
docker ps -q | xargs docker inspect --format '{{.Name}} user={{.Config.User}}'

# Quels ports sont réellement ouverts sur toutes les interfaces ?
sudo ss -tlnp | grep -v 127.0.0.1

# Quels conteneurs ont accès au socket Docker ?
docker ps -q | xargs docker inspect --format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

Et pour chercher les failles connues dans vos images, l’outil open source Trivy les analyse en une commande :

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock:ro aquasec/trivy image --severity HIGH,CRITICAL vaultwarden/server:latest

Le modèle complet

Un service web typique, avec toutes les protections réunies :

services:
  app:
    image: exemple/app:2
    restart: unless-stopped
    user: "2001:2001"
    read_only: true
    tmpfs: [/tmp]
    security_opt: [no-new-privileges:true]
    cap_drop: [ALL]
    env_file: .env
    volumes:
      - ./data:/data
    networks: [web, interne]
    deploy:
      resources:
        limits: { memory: 512M, cpus: "1.0", pids: 200 }

networks:
  web:
    external: true
  interne:
    internal: true

Le générateur docker-compose ajoute ces protections automatiquement quand l’image les supporte.

Récapitulatif

Protection Ligne clé Empêche
Utilisateur dédié user: "2001:2001" d’agir en root, de lire les données des autres
Pas de nouveaux privilèges no-new-privileges:true de regagner des droits
Capacités retirées cap_drop: [ALL] d’utiliser les pouvoirs de root
Lecture seule read_only: true de modifier le programme ou d’installer un outil
Réseau interne internal: true à la base de données de parler à Internet
Ports en local 127.0.0.1:8080:80 de contourner le pare-feu
Secrets à part env_file: .env (chmod 600) de publier un mot de passe dans git
Socket-proxy CONTAINERS: 1 de prendre le contrôle du serveur
Limites memory, pids qu’une appli fasse tomber les autres
Mises à jour différées version majeure épinglée les failles connues et les versions boguées

Prêt à vous lancer ?

Choisissez vos applications et récupérez une installation complète.

Créer mon serveur