Gestion du réseau et des ports Docker
Cette page est dédier à la question suivante :
Comment bien configuré ses conteneurs sur le réseau ?
Portée : Docker Compose, hosts Linux avec UFW comme pare-feu, reverse proxy type Traefik/Caddy/nginx.
Objectif : standardiser une architecture réseau 100 % maîtrisée sur toutes les stacks compose.
1. Comportement par défaut de Docker
1.1 Réseaux créés automatiquement
Par défaut, docker compose up crée un réseau bridge dédié par projet (nommé <nom_projet>_default), distinct du bridge docker0 historique.
Tous les services d'un même fichier compose sont attachés à ce réseau et se résolvent entre eux par nom de service (DNS interne Docker), sans avoir besoin de publier le moindre port.
➡️ Conséquence clé : la communication inter-conteneurs ne nécessite jamais ports:. ports: ne sert qu'à exposer un service en dehors du host Docker (vers le LAN/WAN).
1.2 EXPOSE vs ports:
| Directive | Effet réel | Portée |
|---|---|---|
EXPOSE (Dockerfile) |
Documentation uniquement, aucun binding réseau réel | N/A |
expose: (compose) |
Idem, informatif | N/A |
ports: (compose) |
Publie réellement le port sur une interface du host via NAT | Host + extérieur |
1.3 Docker et iptables : le piège UFW
C'est le point le plus critique en environnement Ops/DevSec.
Quand un conteneur publie un port avec ports:, Docker insère automatiquement des règles dans les chaînes DOCKER et DOCKER-USER de la table nat/filter d'iptables — en amont de la chaîne INPUT gérée par UFW.
Conséquence : un port publié via ports: est accessible depuis l'extérieur même si UFW le bloque explicitement.
UFW ne voit jamais passer ce trafic car Docker route directement via NAT/FORWARD.
Trafic entrant → PREROUTING (Docker DNAT) → FORWARD → DOCKER-USER → DOCKER → conteneur
↑
UFW (INPUT) n'intercepte PAS ce chemin
2. Le comportement de ports: en détail
2.1 Syntaxes
ports:
- "8080:80" # host:container — bind 0.0.0.0 (TOUTES interfaces) par défaut, port ouvert sur iptables
- "127.0.0.1:8080:80" # bind explicite localhost uniquement
- "10.10.10.1:8080:80" # bind sur une IP précise (ex: interface WireGuard)
- "8080:80/udp" # protocole explicite, port ouvert sur iptables
2.2 Le piège du binding implicite
"8080:80" publie sur 0.0.0.0:8080, donc sur toutes les interfaces réseau du host (LAN, WAN, VPN...), pas seulement en local.
C'est la cause n°1 des services/ports exposées par erreur sur Internet.
3. Bonnes pratiques Ops / DevSec
- Principe de moindre exposition : ne publier (
ports:) que ce qui doit être joignable depuis l'extérieur du host. Tout le reste communique via le réseau interne Docker (DNS de service). - Un réseau dédié par stack, jamais le réseau
defaultpartagé pour plusieurs projets sans raison. Nommer explicitement :
networks:
proxy-web:
name: proxy-public # Destiné à exposé les services sur les ports 80/443
backend:
name: <stack>-internal # Destiné a rester en local, jamais exposé publiquement mais reste accessbles pour les autres services internes
internal: true
| Type d’exposition | Type de service |
internal: true |
DB / cache / workers / Clamav |
proxy-public |
Traefik/ Caddy/ nginx |
network_mode: host |
VPN |
Éviter network_mode: host sauf nécessité technique réelle (ex: WireGuard qui a besoin d'accéder aux interfaces réseau du host). Cas contraire, host désactive toute l'isolation réseau Docker.
Convention de nommage des réseaux cohérente sur toutes les stacks (<stack>-internal, proxy-public, vpn-admin...) pour faciliter l'audit avec docker network ls.
Quelle configuration utiliser ?
Avec Docker il existe plusieurs moyens de configurer l'accès aux conteneurs. Chaque solution répond à un besoin bien spécifique.
127.0.0.1).ports:
- "127.0.0.1:8080:8080" # Invisible de l'extérieur, accessible par le pr