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 quelatest; - 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 |