---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Réseau Docker

Le réseau est l'un des aspects les plus souvent mal compris de Docker. Comment deux conteneurs se parlent-ils ? Comment un conteneur communique-t-il avec l'extérieur ? Comment isole-t-on des groupes de conteneurs ? Ce chapitre répond à toutes ces questions, des mécanismes noyau jusqu'aux commandes du quotidien.

```{code-cell} python
:tags: [hide-input]
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from matplotlib.patches import FancyBboxPatch, FancyArrowPatch
import numpy as np
import pandas as pd
import seaborn as sns
import ipaddress
import random

sns.set_theme(style="whitegrid", palette="muted")
plt.rcParams.update({
    "figure.dpi": 120,
    "font.family": "DejaVu Sans",
    "axes.titlesize": 13,
    "axes.labelsize": 11,
})
random.seed(42)
np.random.seed(42)
```

## Les drivers réseau de Docker

Docker propose plusieurs **drivers réseau**, chacun adapté à un cas d'usage différent. Choisir le bon driver est essentiel pour la sécurité, les performances et la connectivité de vos applications.

| Driver | Portée | Description |
|--------|--------|-------------|
| `bridge` | Machine locale | Réseau privé virtuel sur l'hôte (défaut) |
| `host` | Machine locale | Partage le namespace réseau de l'hôte |
| `none` | Machine locale | Aucune interface réseau (isolation totale) |
| `overlay` | Multi-hôtes | Réseau étendu entre plusieurs machines (Docker Swarm) |
| `macvlan` | Machine locale | Attribue une adresse MAC physique au conteneur |
| `ipvlan` | Machine locale | Similaire à macvlan, L2/L3 configurable |

```{admonition} Règle d'or
:class: tip
Pour 90 % des cas de développement local, vous utiliserez le driver **bridge**. En production avec Docker Swarm ou pour des besoins réseau avancés, **overlay** ou **macvlan** entrent en jeu.
```

## Le réseau bridge : au cœur du réseau Docker

### L'interface docker0

Quand Docker est installé, il crée automatiquement une interface réseau virtuelle nommée `docker0` sur l'hôte. C'est le **pont** (bridge) entre les conteneurs et le réseau extérieur.

```bash
# Sur l'hôte, vous pouvez voir l'interface docker0
ip addr show docker0

# Résultat typique :
# 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP>
#     inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
```

L'interface `docker0` a l'adresse `172.17.0.1` et gère le sous-réseau `172.17.0.0/16`. Chaque conteneur lancé sur le réseau bridge par défaut reçoit une adresse IP dans ce plage.

### Les paires veth

La connexion entre un conteneur et le bridge `docker0` se fait via des **paires d'interfaces virtuelles** (veth pairs). C'est comme un câble Ethernet virtuel : un bout est dans le conteneur (nommé `eth0`), l'autre bout est sur l'hôte (nommé `vethXXXXXX`).

```bash
# Sur l'hôte, voir les interfaces veth créées par Docker
ip link show type veth

# Résultat : une paire par conteneur en cours d'exécution
# veth3a1b2c3 correspond à eth0 dans le conteneur
```

### NAT et iptables

Pour permettre à un conteneur d'accéder à Internet, Docker configure automatiquement des règles **iptables** sur l'hôte :

- **MASQUERADE** : le trafic sortant du conteneur est traduit (NAT) avec l'IP de l'hôte
- **FORWARD** : les paquets sont autorisés à transiter via le bridge

```bash
# Voir les règles NAT créées par Docker
sudo iptables -t nat -L -n --line-numbers

# Règle MASQUERADE typique :
# MASQUERADE  all  -- 172.17.0.0/16 !172.17.0.0/16
```

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 8))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Architecture réseau bridge Docker — interface docker0 et paires veth",
             fontsize=13, fontweight="bold", pad=12)

# Zone Internet
inet_box = FancyBboxPatch((5.5, 7.8), 3, 0.9, boxstyle="round,pad=0.1",
                           facecolor="#b3e5fc", edgecolor="#0288d1", linewidth=2)
ax.add_patch(inet_box)
ax.text(7, 8.25, "Internet / Réseau externe", ha="center", va="center",
        fontsize=11, fontweight="bold", color="#01579b")

# Zone hôte
host_box = FancyBboxPatch((0.3, 1.5), 13.4, 6.0, boxstyle="round,pad=0.2",
                           facecolor="#f9fbe7", edgecolor="#827717", linewidth=2, linestyle="--")
ax.add_patch(host_box)
ax.text(7, 7.2, "Hôte Linux (eth0 : 192.168.1.10)", ha="center", va="center",
        fontsize=10, color="#827717", fontstyle="italic")

# eth0 hôte
eth0_box = FancyBboxPatch((5.8, 6.3), 2.4, 0.7, boxstyle="round,pad=0.1",
                           facecolor="#fff9c4", edgecolor="#f9a825", linewidth=2)
ax.add_patch(eth0_box)
ax.text(7, 6.65, "eth0 hôte\n192.168.1.10", ha="center", va="center",
        fontsize=9, fontweight="bold", color="#e65100")

# docker0 bridge
docker0_box = FancyBboxPatch((3.5, 4.8), 7, 0.9, boxstyle="round,pad=0.1",
                              facecolor="#e8f5e9", edgecolor="#2e7d32", linewidth=2.5)
ax.add_patch(docker0_box)
ax.text(7, 5.25, "Bridge docker0 — 172.17.0.1/16   [iptables NAT / MASQUERADE]",
        ha="center", va="center", fontsize=10, fontweight="bold", color="#1b5e20")

# iptables label
ax.text(7, 5.7, "iptables FORWARD + MASQUERADE", ha="center", va="center",
        fontsize=8, color="#558b2f", fontstyle="italic")

# Conteneur 1
c1_box = FancyBboxPatch((0.8, 2.0), 3.2, 2.3, boxstyle="round,pad=0.1",
                         facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=2)
ax.add_patch(c1_box)
ax.text(2.4, 4.0, "Conteneur A", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#0d47a1")
ax.text(2.4, 3.5, "nginx", ha="center", va="center", fontsize=9, color="#0d47a1")
# eth0 conteneur 1
veth1_inner = FancyBboxPatch((1.3, 2.2), 2.2, 0.6, boxstyle="round,pad=0.05",
                              facecolor="#bbdefb", edgecolor="#1565c0", linewidth=1.5)
ax.add_patch(veth1_inner)
ax.text(2.4, 2.5, "eth0 : 172.17.0.2", ha="center", va="center", fontsize=8, color="#0d47a1")

# veth pair côté hôte - conteneur 1
veth1_outer = FancyBboxPatch((1.3, 4.1), 2.2, 0.55, boxstyle="round,pad=0.05",
                              facecolor="#c8e6c9", edgecolor="#388e3c", linewidth=1.5)
ax.add_patch(veth1_outer)
ax.text(2.4, 4.38, "vethABCD (hôte)", ha="center", va="center", fontsize=8, color="#1b5e20")

# "câble" veth
ax.annotate("", xy=(2.4, 4.1), xytext=(2.4, 2.8),
            arrowprops=dict(arrowstyle="-", color="#388e3c", lw=2.5, linestyle="solid"))
ax.text(1.7, 3.45, "paire veth", ha="center", va="center", fontsize=7.5,
        color="#388e3c", rotation=90)

# Conteneur 2
c2_box = FancyBboxPatch((5.3, 2.0), 3.2, 2.3, boxstyle="round,pad=0.1",
                         facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=2)
ax.add_patch(c2_box)
ax.text(6.9, 4.0, "Conteneur B", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#0d47a1")
ax.text(6.9, 3.5, "postgres", ha="center", va="center", fontsize=9, color="#0d47a1")
veth2_inner = FancyBboxPatch((5.8, 2.2), 2.2, 0.6, boxstyle="round,pad=0.05",
                              facecolor="#bbdefb", edgecolor="#1565c0", linewidth=1.5)
ax.add_patch(veth2_inner)
ax.text(6.9, 2.5, "eth0 : 172.17.0.3", ha="center", va="center", fontsize=8, color="#0d47a1")

veth2_outer = FancyBboxPatch((5.8, 4.1), 2.2, 0.55, boxstyle="round,pad=0.05",
                              facecolor="#c8e6c9", edgecolor="#388e3c", linewidth=1.5)
ax.add_patch(veth2_outer)
ax.text(6.9, 4.38, "vethEFGH (hôte)", ha="center", va="center", fontsize=8, color="#1b5e20")
ax.annotate("", xy=(6.9, 4.1), xytext=(6.9, 2.8),
            arrowprops=dict(arrowstyle="-", color="#388e3c", lw=2.5))

# Conteneur 3
c3_box = FancyBboxPatch((9.8, 2.0), 3.2, 2.3, boxstyle="round,pad=0.1",
                         facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=2)
ax.add_patch(c3_box)
ax.text(11.4, 4.0, "Conteneur C", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#0d47a1")
ax.text(11.4, 3.5, "redis", ha="center", va="center", fontsize=9, color="#0d47a1")
veth3_inner = FancyBboxPatch((10.3, 2.2), 2.2, 0.6, boxstyle="round,pad=0.05",
                              facecolor="#bbdefb", edgecolor="#1565c0", linewidth=1.5)
ax.add_patch(veth3_inner)
ax.text(11.4, 2.5, "eth0 : 172.17.0.4", ha="center", va="center", fontsize=8, color="#0d47a1")

veth3_outer = FancyBboxPatch((10.3, 4.1), 2.2, 0.55, boxstyle="round,pad=0.05",
                              facecolor="#c8e6c9", edgecolor="#388e3c", linewidth=1.5)
ax.add_patch(veth3_outer)
ax.text(11.4, 4.38, "vethIJKL (hôte)", ha="center", va="center", fontsize=8, color="#1b5e20")
ax.annotate("", xy=(11.4, 4.1), xytext=(11.4, 2.8),
            arrowprops=dict(arrowstyle="-", color="#388e3c", lw=2.5))

# Connexions vers docker0
for x in [2.4, 6.9, 11.4]:
    ax.annotate("", xy=(x, 4.8), xytext=(x, 4.65),
                arrowprops=dict(arrowstyle="-", color="#1b5e20", lw=2))

# Connexion docker0 vers eth0 hôte
ax.annotate("", xy=(7, 6.3), xytext=(7, 5.7),
            arrowprops=dict(arrowstyle="<->", color="#e65100", lw=2))

# Connexion eth0 hôte vers Internet
ax.annotate("", xy=(7, 7.8), xytext=(7, 7.0),
            arrowprops=dict(arrowstyle="<->", color="#01579b", lw=2))

# Légende port mapping
ax.text(0.5, 1.3, "Port publishing : -p 8080:80 → iptables DNAT 192.168.1.10:8080 → 172.17.0.2:80",
        ha="left", va="center", fontsize=8.5,
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#fff3e0", edgecolor="#e65100"))

plt.tight_layout()
plt.savefig("_static/04_bridge_veth.png", dpi=130, bbox_inches="tight")
plt.show()
```

## Le DNS interne de Docker

L'une des fonctionnalités les plus pratiques de Docker est son **serveur DNS embarqué**. Sur un réseau bridge **personnalisé** (pas le bridge par défaut `docker0`), les conteneurs peuvent se joindre par leur **nom** plutôt que par leur adresse IP.

```bash
# Créer un réseau bridge personnalisé
docker network create mon-reseau

# Lancer deux conteneurs sur ce réseau
docker run -d --name web --network mon-reseau nginx
docker run -d --name db --network mon-reseau postgres

# Dans le conteneur "web", on peut pinger "db" par son nom !
docker exec web ping db
# PING db (172.18.0.3): 56 data bytes
# 64 bytes from 172.18.0.3: seq=0 ttl=64 time=0.089 ms
```

```{admonition} Bridge par défaut vs bridge personnalisé
:class: warning
Sur le réseau `bridge` **par défaut** (`docker0`), le DNS par nom de conteneur **ne fonctionne pas**. Les conteneurs ne peuvent se joindre que par IP. C'est pourquoi il est fortement recommandé de toujours créer un réseau bridge **nommé** pour vos applications. Docker Compose fait cela automatiquement.
```

```{code-cell} python
:tags: [hide-input]
fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# ---- Graphique gauche : bridge par défaut (pas de DNS) ----
ax = axes[0]
ax.set_xlim(0, 10)
ax.set_ylim(0, 8)
ax.axis("off")
ax.set_title("Réseau bridge par défaut\n(docker0) — PAS de DNS",
             fontsize=11, fontweight="bold", color="#c62828")

# Bridge
b1 = FancyBboxPatch((1, 3.5), 8, 0.8, boxstyle="round,pad=0.1",
                     facecolor="#ffcdd2", edgecolor="#c62828", linewidth=2)
ax.add_patch(b1)
ax.text(5, 3.9, "docker0 — 172.17.0.1/16", ha="center", va="center",
        fontsize=9, fontweight="bold", color="#b71c1c")

# Conteneurs
for i, (cx, name, ip) in enumerate([(2.5, "nginx", "172.17.0.2"),
                                      (5.0, "redis", "172.17.0.3"),
                                      (7.5, "app",   "172.17.0.4")]):
    box = FancyBboxPatch((cx - 1.0, 1.5), 2.0, 1.6, boxstyle="round,pad=0.1",
                          facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=1.5)
    ax.add_patch(box)
    ax.text(cx, 2.6, name, ha="center", va="center", fontsize=9, fontweight="bold", color="#0d47a1")
    ax.text(cx, 2.1, ip, ha="center", va="center", fontsize=8, color="#0d47a1")
    ax.annotate("", xy=(cx, 3.5), xytext=(cx, 3.1),
                arrowprops=dict(arrowstyle="-", color="#c62828", lw=2))

# Tentative DNS échoue
ax.annotate("", xy=(5.0, 2.6), xytext=(2.5, 2.6),
            arrowprops=dict(arrowstyle="->", color="#f44336", lw=2, linestyle="dashed"))
ax.text(3.75, 3.0, '✗ ping redis\n(ÉCHEC)', ha="center", va="center",
        fontsize=8, color="#c62828", fontweight="bold")
ax.text(3.75, 2.0, "doit utiliser\n172.17.0.3", ha="center", va="center",
        fontsize=8, color="#c62828", fontstyle="italic")

ax.text(5, 0.6, "Communication uniquement par IP\n→ rigide, fragile si redémarrage",
        ha="center", va="center", fontsize=8.5,
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#ffebee", edgecolor="#f44336"))

# ---- Graphique droit : bridge personnalisé (DNS) ----
ax2 = axes[1]
ax2.set_xlim(0, 10)
ax2.set_ylim(0, 8)
ax2.axis("off")
ax2.set_title("Réseau bridge personnalisé\n(mon-reseau) — DNS intégré",
              fontsize=11, fontweight="bold", color="#2e7d32")

# DNS box
dns_box = FancyBboxPatch((3.5, 6.2), 3, 0.9, boxstyle="round,pad=0.1",
                          facecolor="#e8f5e9", edgecolor="#2e7d32", linewidth=2)
ax2.add_patch(dns_box)
ax2.text(5, 6.65, "DNS Docker\n127.0.0.11", ha="center", va="center",
         fontsize=9, fontweight="bold", color="#1b5e20")

# Bridge
b2 = FancyBboxPatch((1, 4.3), 8, 0.8, boxstyle="round,pad=0.1",
                     facecolor="#c8e6c9", edgecolor="#2e7d32", linewidth=2)
ax2.add_patch(b2)
ax2.text(5, 4.7, "mon-reseau — 172.18.0.1/16", ha="center", va="center",
         fontsize=9, fontweight="bold", color="#1b5e20")

for cx, name, ip in [(2.5, "nginx", "172.18.0.2"),
                      (5.0, "redis", "172.18.0.3"),
                      (7.5, "app",   "172.18.0.4")]:
    box = FancyBboxPatch((cx - 1.0, 2.3), 2.0, 1.6, boxstyle="round,pad=0.1",
                          facecolor="#e3f2fd", edgecolor="#1565c0", linewidth=1.5)
    ax2.add_patch(box)
    ax2.text(cx, 3.4, name, ha="center", va="center", fontsize=9, fontweight="bold", color="#0d47a1")
    ax2.text(cx, 2.9, ip, ha="center", va="center", fontsize=8, color="#0d47a1")
    ax2.annotate("", xy=(cx, 4.3), xytext=(cx, 3.9),
                 arrowprops=dict(arrowstyle="-", color="#2e7d32", lw=2))
    # vers DNS
    ax2.annotate("", xy=(5.0, 6.2), xytext=(cx, 5.1),
                 arrowprops=dict(arrowstyle="-", color="#43a047", lw=1.2, linestyle="dotted"))

# DNS réussit
ax2.annotate("", xy=(5.0, 3.4), xytext=(2.5, 3.4),
            arrowprops=dict(arrowstyle="->", color="#2e7d32", lw=2))
ax2.text(3.75, 3.85, '✓ ping redis\n(SUCCÈS)', ha="center", va="center",
        fontsize=8, color="#2e7d32", fontweight="bold")

ax2.text(5, 1.4, "Communication par nom de conteneur\n→ portable, robuste aux redémarrages",
        ha="center", va="center", fontsize=8.5,
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e8f5e9", edgecolor="#2e7d32"))

plt.tight_layout()
plt.savefig("_static/04_dns_bridge.png", dpi=130, bbox_inches="tight")
plt.show()
```

## Les commandes réseau Docker

### Créer et inspecter des réseaux

```bash
# Lister tous les réseaux
docker network ls

# NETWORK ID     NAME      DRIVER    SCOPE
# a1b2c3d4e5f6   bridge    bridge    local
# f6e5d4c3b2a1   host      host      local
# 123456789abc   none      null      local

# Créer un réseau bridge personnalisé
docker network create mon-reseau

# Créer avec un subnet et gateway spécifiques
docker network create \
  --driver bridge \
  --subnet 192.168.100.0/24 \
  --gateway 192.168.100.1 \
  reseau-prod

# Inspecter un réseau (voir les conteneurs connectés, la config IP...)
docker network inspect mon-reseau
```

### Connecter et déconnecter des conteneurs

```bash
# Connecter un conteneur existant à un réseau
docker network connect mon-reseau mon-conteneur

# Déconnecter
docker network disconnect mon-reseau mon-conteneur

# Un conteneur peut appartenir à plusieurs réseaux simultanément !
docker network connect reseau-frontend mon-conteneur
docker network connect reseau-backend mon-conteneur
```

### Nettoyage

```bash
# Supprimer un réseau (seulement s'il n'est plus utilisé)
docker network rm mon-reseau

# Supprimer tous les réseaux inutilisés
docker network prune
```

## Le réseau host

Avec le driver **host**, le conteneur partage directement le namespace réseau de l'hôte. Il n'y a plus de NAT, plus d'interface virtuelle séparée : le conteneur "est" l'hôte du point de vue réseau.

```bash
# Le conteneur utilise directement le réseau de l'hôte
docker run --network host nginx

# nginx écoute sur le port 80 de l'HÔTE directement
# Pas besoin de -p 80:80 — le port est déjà celui de l'hôte
```

```{admonition} Quand utiliser le réseau host ?
:class: note
Le mode host est utile pour :
- **Performances maximales** : zéro overhead de NAT (trading haute fréquence, analyse réseau)
- **Outils de monitoring réseau** : qui doivent accéder aux interfaces de l'hôte
- **Applications qui gèrent elles-mêmes des ports dynamiques** (ex. : serveurs FTP passif)

**Inconvénient majeur** : le conteneur peut écouter sur n'importe quel port de l'hôte → risque de sécurité. À éviter en production si possible.
```

## Le driver none : isolation totale

```bash
# Conteneur sans aucune connectivité réseau
docker run --network none mon-image

# Utile pour :
# - Traitement de données sensibles (aucune exfiltration possible)
# - Jobs de calcul pur sans besoin réseau
# - Sécurité maximale
```

## Le réseau overlay : Docker multi-hôtes

Le driver **overlay** permet de créer un réseau virtuel s'étendant sur **plusieurs hôtes physiques**. Il utilise le protocole **VXLAN** (Virtual Extensible LAN) pour encapsuler le trafic réseau des conteneurs dans des paquets UDP échangés entre les hôtes.

```{admonition} Overlay et Docker Swarm
:class: note
Le driver overlay nécessite soit **Docker Swarm** (mode cluster intégré à Docker) soit un **key-value store** externe (comme etcd). En pratique, si vous avez besoin d'overlay, vous utilisez Docker Swarm ou vous passez à Kubernetes qui gère cela nativement.
```

```bash
# Initialiser Docker Swarm (mode cluster)
docker swarm init --advertise-addr 192.168.1.10

# Créer un réseau overlay
docker network create --driver overlay mon-overlay

# Les services Swarm peuvent utiliser ce réseau
docker service create --network mon-overlay --name web nginx
```

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(14, 7))
ax.set_xlim(0, 14)
ax.set_ylim(0, 9)
ax.axis("off")
ax.set_title("Réseau Overlay — Encapsulation VXLAN entre hôtes",
             fontsize=13, fontweight="bold", pad=12)

# Réseau VXLAN (overlay)
overlay_box = FancyBboxPatch((0.3, 0.4), 13.4, 8.2, boxstyle="round,pad=0.2",
                              facecolor="#fce4ec", edgecolor="#880e4f", linewidth=1.5,
                              linestyle="--", alpha=0.4)
ax.add_patch(overlay_box)
ax.text(7, 8.3, "Réseau Overlay 10.0.0.0/24 (VXLAN VNI 256) — Virtuel, multi-hôtes",
        ha="center", va="center", fontsize=9, color="#880e4f", fontstyle="italic")

# Réseau physique (underlay)
underlay_box = FancyBboxPatch((0.8, 3.8), 12.4, 0.7, boxstyle="round,pad=0.1",
                               facecolor="#b2dfdb", edgecolor="#00695c", linewidth=2)
ax.add_patch(underlay_box)
ax.text(7, 4.15, "Réseau physique (underlay) — 192.168.1.0/24",
        ha="center", va="center", fontsize=10, fontweight="bold", color="#004d40")

# Hôte 1
h1_box = FancyBboxPatch((0.5, 4.8), 5.8, 3.2, boxstyle="round,pad=0.15",
                          facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2)
ax.add_patch(h1_box)
ax.text(3.4, 7.7, "Hôte 1 — 192.168.1.10", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#1a237e")

# Conteneurs hôte 1
for cx, name, ip in [(1.5, "web-1\n10.0.0.2", "#e3f2fd"),
                      (3.4, "api-1\n10.0.0.3", "#e8f5e9"),
                      (5.3, "db-1\n10.0.0.4",  "#fff3e0")]:
    b = FancyBboxPatch((cx - 0.85, 5.2), 1.7, 1.3, boxstyle="round,pad=0.08",
                        facecolor=ip, edgecolor="#5c6bc0", linewidth=1.5)
    ax.add_patch(b)
    ax.text(cx, 5.88, name, ha="center", va="center", fontsize=8.5,
            fontweight="bold", color="#283593")

# VTEP hôte 1
vtep1 = FancyBboxPatch((1.0, 4.85), 4.8, 0.55, boxstyle="round,pad=0.05",
                         facecolor="#ce93d8", edgecolor="#6a1b9a", linewidth=1.5)
ax.add_patch(vtep1)
ax.text(3.4, 5.12, "VTEP (VXLAN Tunnel Endpoint) — encapsule/décapsule UDP",
        ha="center", va="center", fontsize=8, color="#4a148c")

# Hôte 2
h2_box = FancyBboxPatch((7.7, 4.8), 5.8, 3.2, boxstyle="round,pad=0.15",
                          facecolor="#e8eaf6", edgecolor="#3949ab", linewidth=2)
ax.add_patch(h2_box)
ax.text(10.6, 7.7, "Hôte 2 — 192.168.1.11", ha="center", va="center",
        fontsize=10, fontweight="bold", color="#1a237e")

for cx, name, ip in [(8.7,  "web-2\n10.0.0.5",  "#e3f2fd"),
                      (10.6, "api-2\n10.0.0.6",  "#e8f5e9"),
                      (12.5, "cache\n10.0.0.7",  "#fce4ec")]:
    b = FancyBboxPatch((cx - 0.85, 5.2), 1.7, 1.3, boxstyle="round,pad=0.08",
                        facecolor=ip, edgecolor="#5c6bc0", linewidth=1.5)
    ax.add_patch(b)
    ax.text(cx, 5.88, name, ha="center", va="center", fontsize=8.5,
            fontweight="bold", color="#283593")

vtep2 = FancyBboxPatch((8.2, 4.85), 4.8, 0.55, boxstyle="round,pad=0.05",
                         facecolor="#ce93d8", edgecolor="#6a1b9a", linewidth=1.5)
ax.add_patch(vtep2)
ax.text(10.6, 5.12, "VTEP (VXLAN Tunnel Endpoint) — encapsule/décapsule UDP",
        ha="center", va="center", fontsize=8, color="#4a148c")

# Connexion underlay entre hôtes
ax.annotate("", xy=(7.7, 4.15), xytext=(6.3, 4.15),
            arrowprops=dict(arrowstyle="<->", color="#004d40", lw=2.5))
ax.text(7.0, 4.5, "UDP:4789\n(VXLAN)", ha="center", va="center",
        fontsize=8, color="#004d40", fontweight="bold")

# Connexions VTEP vers réseau physique
for x in [3.4, 10.6]:
    ax.annotate("", xy=(x, 4.5), xytext=(x, 4.85),
                arrowprops=dict(arrowstyle="-", color="#6a1b9a", lw=2))

# Communication overlay (arc)
ax.annotate("", xy=(8.7, 6.3), xytext=(5.3, 6.3),
            arrowprops=dict(arrowstyle="<->", color="#880e4f", lw=2,
                            connectionstyle="arc3,rad=-0.3"))
ax.text(7.0, 7.1, "ping cache depuis web-1\n10.0.0.7 (transparent !)", ha="center", va="center",
        fontsize=8, color="#880e4f", fontweight="bold",
        bbox=dict(boxstyle="round,pad=0.2", facecolor="white", edgecolor="#880e4f", alpha=0.9))

# Légende
ax.text(7, 0.2,
        "Du point de vue des conteneurs, ils sont sur le même réseau — l'encapsulation VXLAN est transparente",
        ha="center", va="center", fontsize=9, color="#004d40",
        bbox=dict(boxstyle="round,pad=0.3", facecolor="#e0f2f1", edgecolor="#00695c"))

plt.tight_layout()
plt.savefig("_static/04_overlay_vxlan.png", dpi=130, bbox_inches="tight")
plt.show()
```

## Publication de ports

La publication de ports permet d'accéder à un conteneur depuis l'extérieur de l'hôte. Docker crée des règles iptables pour rediriger le trafic entrant vers le bon conteneur.

```bash
# -p HOST_PORT:CONTAINER_PORT
docker run -p 8080:80 nginx
# Le port 80 du conteneur est accessible via le port 8080 de l'hôte

# Lier à une IP spécifique (sécurité : écoute uniquement sur localhost)
docker run -p 127.0.0.1:8080:80 nginx
# Accessible uniquement depuis la machine locale, pas depuis le réseau

# Port aléatoire sur l'hôte (-P, expose tous les ports déclarés dans l'image)
docker run -P nginx
# Docker choisit un port disponible (ex: 32768)

# Voir les ports publiés
docker port mon-conteneur
# 80/tcp -> 0.0.0.0:8080
```

```{admonition} Liaison à 127.0.0.1 — Bonne pratique de sécurité
:class: tip
En développement, il est courant d'utiliser `-p 8080:80` qui lie le port à toutes les interfaces (`0.0.0.0`). En production ou sur un serveur exposé, préférez `-p 127.0.0.1:8080:80` et mettez un reverse proxy (nginx, Traefik) devant. Cela évite d'exposer accidentellement des services de développement (bases de données, outils d'administration) sur Internet.
```

## Simulation Python : allocation d'IP dans un subnet Docker

Docker utilise le module réseau du noyau Linux pour allouer les adresses IP. Voici une simulation de la logique d'allocation en Python pur avec le module `ipaddress` de la bibliothèque standard.

```{code-cell} python
import ipaddress
import random
from collections import OrderedDict

class DockerIPAMSimulator:
    """Simulation simplifiée du gestionnaire d'adresses IP (IPAM) de Docker."""

    def __init__(self, subnet: str, gateway: str = None):
        self.network = ipaddress.IPv4Network(subnet, strict=False)
        self.hosts = list(self.network.hosts())

        # Le gateway est typiquement la première IP (.1)
        if gateway:
            self.gateway = ipaddress.IPv4Address(gateway)
        else:
            self.gateway = self.hosts[0]

        # Réserver le gateway
        self.allocated: OrderedDict = OrderedDict()
        self.allocated[str(self.gateway)] = "gateway (docker0)"
        self.next_idx = 1  # Commence après le gateway

    def allocate(self, container_name: str) -> str:
        """Alloue la prochaine IP disponible à un conteneur."""
        while self.next_idx < len(self.hosts):
            ip = self.hosts[self.next_idx]
            self.next_idx += 1
            ip_str = str(ip)
            if ip_str not in self.allocated:
                self.allocated[ip_str] = container_name
                return ip_str
        raise RuntimeError("Plus d'adresses IP disponibles dans ce subnet !")

    def release(self, container_name: str):
        """Libère l'IP d'un conteneur (mais Docker ne la réutilise pas immédiatement)."""
        for ip, name in list(self.allocated.items()):
            if name == container_name:
                del self.allocated[ip]
                print(f"  IP {ip} libérée (conteneur '{container_name}' supprimé)")
                return
        print(f"  Conteneur '{container_name}' non trouvé")

    def status(self):
        """Affiche l'état actuel des allocations."""
        print(f"\nRéseau : {self.network}")
        print(f"Capacité : {self.network.num_addresses - 2} hôtes disponibles")
        print(f"Allocations actuelles ({len(self.allocated)}) :")
        for ip, name in self.allocated.items():
            role = " [GATEWAY]" if name == f"gateway (docker0)" else ""
            print(f"  {ip:<18} → {name}{role}")
        libres = self.network.num_addresses - 2 - len(self.allocated)
        print(f"Adresses libres : {libres}")

    def __repr__(self):
        return f"DockerIPAMSimulator(subnet={self.network}, allocated={len(self.allocated)})"


# Simulation du réseau bridge par défaut de Docker
print("=" * 55)
print("Simulation IPAM — Réseau bridge par défaut (docker0)")
print("=" * 55)

ipam = DockerIPAMSimulator("172.17.0.0/16", gateway="172.17.0.1")

conteneurs = ["nginx-web", "postgres-db", "redis-cache", "app-backend", "worker-1"]
ips_allouees = {}

for nom in conteneurs:
    ip = ipam.allocate(nom)
    ips_allouees[nom] = ip
    print(f"  [+] Conteneur '{nom}' → {ip}")

ipam.status()

print("\n--- Suppression de quelques conteneurs ---")
ipam.release("redis-cache")
ipam.release("nginx-web")

print("\n--- Nouveau conteneur (réutilisation d'IP ?) ---")
# Docker alloue séquentiellement, la nouvelle IP est après les précédentes
new_ip = ipam.allocate("nouveau-service")
print(f"  [+] 'nouveau-service' → {new_ip}")
print("  Note : Docker n'a PAS réutilisé les IPs libérées (allocation séquentielle)")

ipam.status()
```

```{code-cell} python
:tags: [hide-input]
# Visualisation : distribution des adresses IP dans différents réseaux Docker

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# --- Graphique 1 : taille des réseaux selon le masque ---
ax1 = axes[0]
masques = [8, 16, 20, 24, 28, 30]
tailles = [ipaddress.IPv4Network(f"172.0.0.0/{m}").num_addresses - 2 for m in masques]
labels = [f"/{m}\n({t:,} hôtes)" for m, t in zip(masques, tailles)]

colors = plt.cm.Blues(np.linspace(0.4, 0.9, len(masques)))
bars = ax1.barh(labels, tailles, color=colors, edgecolor="white")
ax1.set_xscale("log")
ax1.set_xlabel("Nombre d'hôtes disponibles (échelle log)")
ax1.set_title("Taille des réseaux selon le masque CIDR\n(subnets Docker typiques)",
              fontweight="bold")

# Annotations
for bar, t in zip(bars, tailles):
    ax1.text(bar.get_width() * 1.1, bar.get_y() + bar.get_height()/2,
             f"{t:,}", va="center", fontsize=9)

# Marquer les subnets Docker courants
for label, t, m in zip(labels, tailles, masques):
    if m in [16, 24]:
        idx = masques.index(m)
        ax1.get_yticklabels()[idx].set_color("#c62828")
        ax1.get_yticklabels()[idx].set_fontweight("bold")

ax1.text(0.98, 0.02, "En rouge : subnets Docker les plus courants (/16 et /24)",
         transform=ax1.transAxes, ha="right", va="bottom", fontsize=8, color="#c62828")
ax1.set_xlim(1, max(tailles) * 3)
sns.despine(ax=ax1)

# --- Graphique 2 : simulation d'allocations dans plusieurs réseaux ---
ax2 = axes[1]

reseaux_sim = {
    "bridge (docker0)\n172.17.0.0/16": {"subnet": "172.17.0.0/16", "conteneurs": 8},
    "mon-reseau\n172.18.0.0/16":       {"subnet": "172.18.0.0/16", "conteneurs": 5},
    "reseau-prod\n192.168.100.0/24":   {"subnet": "192.168.100.0/24", "conteneurs": 3},
    "reseau-test\n10.10.0.0/24":       {"subnet": "10.10.0.0/24", "conteneurs": 12},
}

categories = []
utilises = []
libres_list = []
couleurs_util = []
couleurs_libr = []

for nom, cfg in reseaux_sim.items():
    net = ipaddress.IPv4Network(cfg["subnet"])
    total = net.num_addresses - 2
    utilise = cfg["conteneurs"] + 1  # +1 pour le gateway
    libre = total - utilise
    categories.append(nom)
    utilises.append(utilise)
    libres_list.append(min(libre, total))

x = np.arange(len(categories))
w = 0.5

bars_u = ax2.bar(x, utilises, w, label="IPs allouées (gateway + conteneurs)",
                  color="#1565c0", alpha=0.85)
bars_l = ax2.bar(x, libres_list, w, bottom=utilises, label="IPs disponibles",
                  color="#a5d6a7", alpha=0.85)

ax2.set_xticks(x)
ax2.set_xticklabels(categories, fontsize=8)
ax2.set_yscale("log")
ax2.set_ylabel("Nombre d'adresses (échelle log)")
ax2.set_title("Utilisation des adresses IP\npar réseau Docker", fontweight="bold")
ax2.legend(fontsize=8)

for bar, val in zip(bars_u, utilises):
    ax2.text(bar.get_x() + bar.get_width()/2, val * 0.6,
             str(val), ha="center", va="center", fontsize=9, color="white", fontweight="bold")

sns.despine(ax=ax2)

plt.tight_layout()
plt.savefig("_static/04_ipam_simulation.png", dpi=130, bbox_inches="tight")
plt.show()
```

## Récapitulatif — Choisir son driver réseau

```{code-cell} python
:tags: [hide-input]
fig, ax = plt.subplots(figsize=(13, 5))
ax.axis("off")
ax.set_title("Choisir le bon driver réseau Docker", fontsize=13, fontweight="bold", pad=10)

headers = ["Driver", "Isolation", "DNS par nom", "Multi-hôtes", "Performances", "Cas d'usage typique"]
rows = [
    ["bridge (défaut)", "Partielle*", "✗ Non", "✗ Non", "Bonne", "Dev local, un seul hôte"],
    ["bridge nommé",    "Oui",        "✓ Oui", "✗ Non", "Bonne", "Docker Compose, prod mono-hôte"],
    ["host",            "Aucune",     "N/A",   "✗ Non", "Maximale", "Monitoring, hautes perfs"],
    ["none",            "Totale",     "N/A",   "✗ Non", "N/A",   "Calcul pur, sécurité max"],
    ["overlay",         "Oui",        "✓ Oui", "✓ Oui", "Bonne", "Docker Swarm multi-nœuds"],
    ["macvlan",         "Oui",        "✗ Non", "✗ Non", "Maximale", "Intégration réseau physique"],
]

colors_map = {
    "✓ Oui": "#c8e6c9", "✗ Non": "#ffcdd2", "Oui": "#c8e6c9",
    "Aucune": "#ffcdd2", "Totale": "#e8f5e9", "Partielle*": "#fff9c4",
    "N/A": "#f5f5f5", "Maximale": "#b3e5fc", "Bonne": "#dcedc8",
}

col_widths = [0.18, 0.12, 0.14, 0.14, 0.14, 0.28]
x_starts = [0.01]
for w in col_widths[:-1]:
    x_starts.append(x_starts[-1] + w)

# En-têtes
for i, (h, x, w) in enumerate(zip(headers, x_starts, col_widths)):
    rect = FancyBboxPatch((x, 0.82), w - 0.01, 0.14, transform=ax.transAxes,
                           boxstyle="round,pad=0.01", facecolor="#37474f",
                           edgecolor="white", linewidth=1, clip_on=False)
    ax.add_patch(rect)
    ax.text(x + w/2, 0.89, h, transform=ax.transAxes, ha="center", va="center",
            fontsize=8.5, fontweight="bold", color="white")

# Lignes
row_height = 0.13
for r_idx, row in enumerate(rows):
    y = 0.82 - (r_idx + 1) * row_height
    bg = "#fafafa" if r_idx % 2 == 0 else "#f0f4f8"
    for c_idx, (cell, x, w) in enumerate(zip(row, x_starts, col_widths)):
        cell_color = colors_map.get(cell, bg)
        rect = FancyBboxPatch((x, y), w - 0.01, row_height - 0.01,
                               transform=ax.transAxes,
                               boxstyle="round,pad=0.005", facecolor=cell_color,
                               edgecolor="#cccccc", linewidth=0.5, clip_on=False)
        ax.add_patch(rect)
        ax.text(x + w/2, y + row_height/2, cell, transform=ax.transAxes,
                ha="center", va="center", fontsize=8,
                color="#212121" if cell_color != "#37474f" else "white")

ax.text(0.01, 0.02, "* bridge par défaut : les conteneurs se voient par IP mais pas par nom",
        transform=ax.transAxes, fontsize=7.5, color="#666666", fontstyle="italic")

plt.tight_layout()
plt.savefig("_static/04_recap_drivers.png", dpi=130, bbox_inches="tight")
plt.show()
```

## Points clés à retenir

- Docker crée une interface `docker0` (bridge) sur l'hôte et connecte chaque conteneur via des **paires veth**
- Le NAT (iptables MASQUERADE) permet aux conteneurs d'accéder à Internet
- Sur un réseau bridge **personnalisé**, Docker fournit un **DNS interne** permettant la résolution par nom de conteneur
- Le réseau bridge **par défaut** (`docker0`) ne supporte pas le DNS par nom — toujours créer un réseau nommé
- `docker network create/ls/inspect/connect/disconnect` sont les commandes essentielles
- La publication de ports (`-p HOST:CONTAINER`) crée des règles iptables DNAT
- Le driver **overlay** utilise VXLAN pour étendre un réseau sur plusieurs machines (Docker Swarm)
- En développement, Docker Compose crée automatiquement un réseau bridge nommé pour chaque projet
