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.

Hide code cell source

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

Règle d’or

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.

# 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).

# 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

# 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

Hide code cell source

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()
_images/9f61a20b9e59245408fc9aafa65d71d26713984daccaa276a0db88eacdb6c5f7.png

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.

# 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

Bridge par défaut vs bridge personnalisé

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.

Hide code cell source

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()
_images/04ef7a815bfa0b86137ab741482649f5363282c10079ceb615ef107efe51af4f.png

Les commandes réseau Docker#

Créer et inspecter des réseaux#

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

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

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

# 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

Quand utiliser le réseau host ?

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#

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

Overlay et Docker Swarm

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.

# 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

Hide code cell source

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()
_images/332d2c48d1bdd143ca0b385979b75215c8dc5a35db45e94995d8ab29093e212b.png

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.

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

Liaison à 127.0.0.1 — Bonne pratique de sécurité

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.

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()
=======================================================
Simulation IPAM — Réseau bridge par défaut (docker0)
=======================================================
  [+] Conteneur 'nginx-web' → 172.17.0.2
  [+] Conteneur 'postgres-db' → 172.17.0.3
  [+] Conteneur 'redis-cache' → 172.17.0.4
  [+] Conteneur 'app-backend' → 172.17.0.5
  [+] Conteneur 'worker-1' → 172.17.0.6

Réseau : 172.17.0.0/16
Capacité : 65534 hôtes disponibles
Allocations actuelles (6) :
  172.17.0.1         → gateway (docker0) [GATEWAY]
  172.17.0.2         → nginx-web
  172.17.0.3         → postgres-db
  172.17.0.4         → redis-cache
  172.17.0.5         → app-backend
  172.17.0.6         → worker-1
Adresses libres : 65528

--- Suppression de quelques conteneurs ---
  IP 172.17.0.4 libérée (conteneur 'redis-cache' supprimé)
  IP 172.17.0.2 libérée (conteneur 'nginx-web' supprimé)

--- Nouveau conteneur (réutilisation d'IP ?) ---
  [+] 'nouveau-service' → 172.17.0.7
  Note : Docker n'a PAS réutilisé les IPs libérées (allocation séquentielle)

Réseau : 172.17.0.0/16
Capacité : 65534 hôtes disponibles
Allocations actuelles (5) :
  172.17.0.1         → gateway (docker0) [GATEWAY]
  172.17.0.3         → postgres-db
  172.17.0.5         → app-backend
  172.17.0.6         → worker-1
  172.17.0.7         → nouveau-service
Adresses libres : 65529

Hide code cell source

# 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()
_images/3003a97d8867bfaae04fb27cff427059cd6559224e0fa12cec5e3b997f8555c9.png

Récapitulatif — Choisir son driver réseau#

Hide code cell source

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()
_images/0b66a8658b2fbf917859b7beaf472b43936b55712b8c52c2a9b47ec71bfbd7c5.png

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