# Gestion du réseau et des ports Docker

> <span class="iNqyIf" data-complete="true" data-copy-service-computed-style="font-family: "Google Sans", sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);" data-sfc-cp="" data-sfc-root="ep"><span class="rQesXe MPyX">Cette page est dédier à la question suivante : </span></span>
> 
> ##### <span class="iNqyIf" data-complete="true" data-copy-service-computed-style="font-family: "Google Sans", sans-serif; font-size: 16px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);" data-sfc-cp="" data-sfc-root="ep"><span class="rQesXe MPyX">Comment bien configuré ses conteneurs sur le réseau ?</span></span>

<p class="callout info"><span style="text-decoration: underline;">**Portée :**</span> Docker Compose, hosts Linux avec UFW comme pare-feu, reverse proxy type Traefik/Caddy/nginx.   
<span style="text-decoration: underline;">**Objectif :**</span> standardiser une architecture réseau 100 % maîtrisée sur toutes les stacks compose.</p>


#### 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:`

<div class="overflow-x-auto w-full px-2 mb-6 print:overflow-x-visible" data-sourcepos="17:1-21:108;903-1195" dir="ltr" id="bkmrk-directive-effet-r%C3%A9el"><table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">Directive</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">Effet réel</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">Portée</th></tr></thead><tbody><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`EXPOSE` (Dockerfile)</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Documentation uniquement, aucun binding réseau réel</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">N/A</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`expose:` (compose)</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Idem, informatif</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">N/A</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`ports:` (compose)</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Publie réellement le port sur une interface du **host** via NAT</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Host + extérieur</td></tr></tbody></table>

</div>
##### 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.

<p class="callout warning">**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.</p>

```
Trafic entrant → PREROUTING (Docker DNAT) → FORWARD → DOCKER-USER → DOCKER → conteneur
                                                ↑
                                    UFW (INPUT) n'intercepte PAS ce chemin
```

---

<div class="Fsg96" data-complete="true" data-copy-service-computed-style="font-family: "Google Sans", sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);" data-processed="true" data-sfc-cp="" data-sfc-inited="2" data-sfc-root="ep" id="bkmrk--2" jsaction="rcuQ6b:&ndmdjb_1f|npT2md" jscontroller="KHhJQ#U8DOt" jsuid="ndmdjb_1f"></div>#### 2. Le comportement de `ports:` en détail

##### 2.1 Syntaxes

```yaml
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
```


<div class="Fsg96" data-complete="true" data-copy-service-computed-style="font-family: "Google Sans", sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);" data-processed="true" data-sfc-cp="" data-sfc-inited="2" data-sfc-root="ep" id="bkmrk-" jsaction="rcuQ6b:&ndmdjb_1d|npT2md" jscontroller="KHhJQ#U8DOt" jsuid="ndmdjb_1d"></div>##### 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.

<p class="callout danger">**C'est la cause n°1 des services/ports exposées par erreur sur Internet.**</p>

---

#### 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 `default` partagé pour plusieurs projets sans raison. Nommer explicitement :

```yaml
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
```

<table border="1" id="bkmrk-type-d%E2%80%99exposition%C2%A0-t" style="border-collapse: collapse; width: 100%; height: 89.3907px;"><colgroup><col style="width: 44.8153%;"></col><col style="width: 55.1847%;"></col></colgroup><tbody><tr style="height: 29.7969px;"><td style="height: 29.7969px;">**Type d’exposition** </td><td class="align-center" style="height: 29.7969px;">**Type de service**</td></tr><tr style="height: 29.7969px;"><td class="align-left" style="height: 29.7969px;">`internal: true`</td><td style="height: 29.7969px;">DB / cache / workers / Clamav</td></tr><tr style="height: 29.7969px;"><td class="align-left" style="height: 29.7969px;">`proxy-public`</td><td style="height: 29.7969px;">Traefik/ Caddy/ nginx</td></tr><tr><td class="align-left">`network_mode: host`</td><td>VPN</td></tr></tbody></table>

**É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`.

---

<div class="Fsg96" data-complete="true" data-copy-service-computed-style="font-family: "Google Sans", sans-serif; font-size: 14px; font-weight: 400; margin: 0px; text-decoration: none; border-bottom: 0px rgb(230, 232, 240);" data-processed="true" data-sfc-cp="" data-sfc-inited="2" data-sfc-root="ep" id="bkmrk--5" jsaction="rcuQ6b:&ndmdjb_1f|npT2md" jscontroller="KHhJQ#U8DOt" jsuid="ndmdjb_1f"></div>#### 4. Commandes utiles pour l'audit

##### 4.1 Depuis Docker

```bash
# Ports publiés par tous les conteneurs actifs
docker ps --format "table {{.Names}}\t{{.Ports}}"

# Ports publiés par un conteneur précis
docker port <container>

# Lister les réseaux et repérer les réseaux "trop partagés"
docker network ls

# Détail d'un réseau : conteneurs attachés, subnet, internal ou non
docker network inspect <network>

# Vue globale via l'API compose
docker compose ps
```

##### 4.2 Depuis le host (vérité terrain, indépendante de Docker)

```bash
# Tous les sockets en écoute, avec le PID/process
ss -tulpn

# Équivalent netstat
sudo netstat -tulpn

# Règles NAT injectées par Docker (bindings réels)
sudo iptables -t nat -L DOCKER -n --line-numbers

# Règles de filtrage custom (celles qui priment sur UFW)
sudo iptables -L DOCKER-USER -n --line-numbers

# Vérification externe depuis une autre machine (la vraie preuve)
nmap -Pn -p- <ip_du_host>
```

<p class="callout info">`iptables` sur le host est la seule vérité fiable : la lecture des `docker-compose.yml` seule ne garantit pas ce qui est réellement exposé (héritage de configs, overrides, etc.).</p>

Explication de la sortie affichée par la commande `sudo iptables -t nat -L DOCKER -n -v --line-numbers` :  
**Lecture des colonnes** (avec `-v`, qui ajoute compteurs et interfaces — utile pour vérifier qu'une règle est réellement traversée) :

<div class="overflow-x-auto w-full px-2 mb-6 print:overflow-x-visible" data-sourcepos="138:1-150:249;6200-7573" dir="ltr" id="bkmrk-colonne-significatio"><table class="min-w-full border-collapse text-sm leading-[1.7] whitespace-normal"><thead class="text-left"><tr><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">Colonne</th><th class="text-text-100 border-b-0.5 border-[hsl(var(--border-300)/0.6)] py-2 pr-4 align-top font-bold" scope="col">Signification</th></tr></thead><tbody><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`num`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Numéro de la règle dans la chaîne (utile pour cibler une règle précise avec `-D DOCKER-USER <num>`)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`pkts`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Nombre de paquets ayant matché cette règle depuis le dernier reset des compteurs — un `0` persistant indique une règle jamais déclenchée (donc potentiellement inutile ou mal placée)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`bytes`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Volume total de données ayant matché la règle (utile pour repérer un trafic anormal sur un port)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`target`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Action appliquée : `DNAT` (redirection vers l'IP/port du conteneur), `ACCEPT`, `DROP`, `RETURN`, ou le nom d'une sous-chaîne (ex: un nom de réseau Docker généré)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`prot`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Protocole concerné : `tcp`, `udp`, ou `all`</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`opt`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Options additionnelles de la règle (généralement `--` si aucune)</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`in`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Interface réseau d'entrée du paquet (ex: `eth0`, `wg0`, `br-xxxxx`) — `*` = toutes interfaces</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`out`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Interface réseau de sortie — `*` = toutes interfaces</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`source`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Adresse IP/subnet source du paquet — `0.0.0.0/0` = n'importe quelle source</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`destination`</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Adresse IP/subnet de destination — `0.0.0.0/0` = n'importe quelle destination</td></tr><tr><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">`to:` (en fin de ligne, table `nat`)</td><td class="border-b-0.5 border-[hsl(var(--border-300)/0.3)] py-2 pr-4 align-top">Présent uniquement sur les règles `DNAT` : indique l'IP:port réel du conteneur vers lequel le trafic est redirigé (ex: `to:172.17.0.3:80`) — c'est cette colonne qui révèle le binding effectif d'un `ports:`</td></tr></tbody></table>

</div>Point de vigilance particulier : sur la chaîne `DOCKER` (table `nat`), une règle avec `source 0.0.0.0/0` et une colonne `to:` renseignée confirme qu'un port est bien publié en NAT vers un conteneur — indépendamment de ce qu'affiche UFW, qui n'intercepte pas ce chemin.

---

#### 5 Exemples par cas d'usage

##### 5.1 Postgres non exposé

```yaml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
    networks:
      - bdd-internal
    # AUCUN "ports:" — accessible uniquement via le réseau interne

  app:
    image: mon-app
    networks:
      - bdd-internal
    depends_on:
      - postgres
    # se connecte à "postgres:5432" via le DNS interne Docker

networks:
  bdd-internal:
    internal: true   # aucune route sortante/entrante possible
```

##### 5.2 Reverse proxy avec réseau dédié

```yaml
services:
  traefik:
    image: traefik:v3
    ports:
      - "80:80"
      - "443:443"        # seul conteneur exposé sur 0.0.0.0
    networks:
      - proxy-public
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro

  webapp:
    image: mon-webapp
    networks:
      - proxy-public   # visible par Traefik
      - bdd-internal   # accès DB
    labels:
      - traefik.enable=true
      - traefik.http.routers.webapp.rule=Host(`app.example.com`)
    # AUCUN "ports:" sur webapp — tout passe par Traefik

  postgres:
    image: postgres:16
    networks:
      - bdd-internal   # jamais sur proxy-public

networks:
  proxy-public:
    name: proxy-public
    external: true   # réseau partagé, créé une fois, rejoint par chaque stack
  bdd-internal:
    internal: true
```

Le service `webapp` n'a pas de `ports:` du tout : il n'est joignable que via Traefik, lui-même seul point d'entrée public du host.

<span style="text-decoration: underline;">**Cas particulier :**</span> le reverse proxy tourne \*\*directement sur le host\*\* (pas en conteneur). Il ne peut pas "rejoindre" un network Docker comme le ferait un conteneur — deux approches valables selon le niveau de rigueur voulu.

Solution : Chaque service publie son port uniquement sur `127.0.0.1`, Apache proxy vers localhost :

```yaml
services:
  webapp:
    image: mon-webapp
    ports:
      - "127.0.0.1:8081:80"   # jamais 0.0.0.0
    networks:
      - bdd-internal
```

```
<VirtualHost *:443>
    ServerName app.example.com
    ProxyPass / http://127.0.0.1:8081/
    ProxyPassReverse / http://127.0.0.1:8081/
</VirtualHost>
```

Avantage : simple, aucune règle iptables custom nécessaire (un socket bindé en loopback n'est routable par personne d'autre que le host lui-même).

##### 5.3 VPN WireGuard : accès admin sans passer par le reverse proxy

<p class="callout info">Cas où un service d'administration (Portainer, dashboard interne, Home Assistant...) ne doit être joignable \*\*que via le tunnel WireGuard\*\*, jamais via Internet public, même pas via le reverse proxy.</p>

```yaml
services:
  wireguard:
    image: linuxserver/wireguard
    network_mode: host   # nécessaire pour le routing VPN natif
    cap_add:
      - NET_ADMIN
      - SYS_MODULE
    volumes:
      - ./wg-config:/config

  admin-panel:
    image: mon-admin-panel
    ports:
      - "10.10.10.1:9000:9000"   # IP wireguard + bind UNIQUEMENT sur l'IP de l'interface wg0
    networks:
      - backend
```

Complément côté firewall (le bind seul limite déjà l'exposition, mais on verrouille aussi explicitement) :

```bash
# Autoriser uniquement depuis le subnet WireGuard
sudo iptables -I DOCKER-USER -i wg0 -s 10.10.10.0/24 -j ACCEPT
sudo iptables -I DOCKER-USER -p tcp --dport 9000 -j DROP
```

##### 5.4 Debug local uniquement

Pattern générique pour tout service qu'on veut inspecter temporairement sans l'exposer :

```yaml
    ports:
      - "127.0.0.1:PORT:PORT"
```

<p class="callout success">Accessible uniquement en `curl localhost:PORT` depuis le host lui-même — jamais depuis le LAN, le WAN ou le VPN.</p>