0%
Réseaux et volumes Docker

Réseaux et volumes

Comment les conteneurs se parlent et ce qui leur survit.

10-15 min

Réseaux et volumes Docker

Deux questions reviennent invariablement dès qu’on dépasse le conteneur isolé, et elles produisent à elles seules la majorité des messages d’erreur incompréhensibles des premiers mois.

La première : « pourquoi mon application n’arrive pas à joindre sa base de données ? » On a lancé les deux conteneurs, chacun tourne, docker ps est vert, et l’API répond obstinément connection refused sur localhost:5432. Le réflexe est de chercher une adresse IP, de la coder en dur, de constater que ça marche — jusqu’au prochain redémarrage où l’IP a changé. Le problème n’était pas l’adresse : c’est que localhost, dans un conteneur, ne désigne pas la même machine que sur l’hôte.

La seconde : « pourquoi mes données ont disparu ? » Une base Postgres remplie pendant deux jours de tests, un docker compose down -v tapé sans réfléchir, et tout est parti. Ce n’est pas un accident : c’est le comportement documenté. Un conteneur est éphémère par conception, et sa couche d’écriture meurt avec lui. La persistance n’est jamais implicite, elle se déclare.

Ces deux questions ont la même racine : un conteneur est isolé par défaut, sur le réseau comme sur le stockage. Cette isolation est ce qui rend Docker reproductible ; les réseaux et les volumes sont les deux mécanismes explicites qui la percent, exactement là où vous le décidez.

Les pilotes réseau, et lequel choisir

PiloteUsageRemarque
bridgeLe défaut. Réseau privé virtuel sur l’hôte.Le seul nécessaire dans 95 % des cas.
hostLe conteneur partage la pile réseau de l’hôte.Aucune isolation, aucun mappage. Linux uniquement.
noneAucun réseau, sauf l’interface de bouclage.Traitement purement local.
macvlanAdresse MAC propre sur le réseau physique.Intégration réseau particulière.
overlayRéseau réparti sur plusieurs hôtes.Réservé aux clusters Swarm.
# Les reseaux existants. bridge, host et none sont crees a l'installation.
docker network ls

# Le detail : plage d'adresses, passerelle, conteneurs connectes
docker network inspect bridge

Le pilote host fait gagner quelques microsecondes de latence en supprimant la traduction d’adresses, au prix de toute isolation : le conteneur occupe directement les ports de l’hôte, avec les collisions que cela implique. C’est un recours, pas une optimisation par défaut. Et il ne vaut que sur Linux : sur macOS et Windows, la pile « hôte » serait celle de la VM intermédiaire.

Le point qui débloque tout : bridge par défaut contre bridge personnalisé

Voici la différence la plus utile de cette leçon, et elle est presque toujours ignorée.

Tout conteneur lancé sans précision rejoint le réseau bridge par défaut. Sur ce réseau, la résolution DNS par nom de conteneur ne fonctionne pas.

docker run -d --name serveur nginx:1.27-alpine
docker run --rm alpine:3.20 ping -c 2 serveur
# ping: bad address 'serveur'   ->  echec attendu

Créez un réseau vous-même, et le comportement change complètement : Docker y attache un serveur DNS interne qui résout les noms de conteneurs.

docker network create app-net

docker run -d --name serveur --network app-net nginx:1.27-alpine
docker run --rm --network app-net alpine:3.20 ping -c 2 serveur
# 2 packets transmitted, 2 packets received  ->  succes

# La requete HTTP passe sur le port INTERNE (80), sans qu'aucun
# port n'ait ete publie sur l'hote.
docker run --rm --network app-net alpine:3.20 \
  sh -c "apk add --no-cache curl >/dev/null && curl -s -o /dev/null -w '%{http_code}\n' http://serveur"

La règle à retenir : créez toujours un réseau nommé pour vos applications. C’est ce que Docker Compose fait automatiquement — le réseau <projet>_default — et c’est pourquoi, à la leçon précédente, l’API joignait sa base à l’adresse db:5432 sans configuration. Cela règle du même coup le problème des adresses IP : le DNS interne suit les conteneurs, et un service recréé avec une nouvelle IP reste joignable sous le même nom, aussitôt.

Un conteneur peut être connecté à plusieurs réseaux, à chaud :

docker network create back-net
docker network connect back-net serveur
docker network disconnect back-net serveur

docker inspect serveur --format '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}'

Publier un port : ce que -p fait exactement

-p 8080:80 n’ouvre pas le port 80 du conteneur — il était déjà ouvert à l’intérieur de son réseau. Ce que fait -p, c’est installer une règle de traduction d’adresses sur l’hôte. L’ordre hôte:conteneur se retient par le sens de la circulation : on vient de l’hôte, on va vers le conteneur.

# Port 8080 de l'hote -> port 80 du conteneur
docker run -d -p 8080:80 nginx:1.27-alpine

# Se lier uniquement a l'interface de bouclage : accessible depuis
# la machine, INVISIBLE depuis le reseau local.
docker run -d -p 127.0.0.1:8080:80 nginx:1.27-alpine

# Laisser Docker choisir un port libre sur l'hote
docker run -d -p 80 nginx:1.27-alpine

# Publier tous les ports declares par EXPOSE
docker run -d -P nginx:1.27-alpine

# Voir les correspondances effectives
docker port <conteneur>

La deuxième forme mérite une insistance particulière, parce qu’elle corrige un problème de sécurité que beaucoup découvrent trop tard. -p 8080:80 se lie par défaut à 0.0.0.0, c’est-à-dire à toutes les interfaces, adresse publique comprise. Et sur Linux, Docker écrit ses règles de traduction dans une chaîne iptables évaluée avant celles d’un pare-feu applicatif comme ufw : un port publié par Docker peut donc rester joignable depuis Internet alors que ufw status affirme le bloquer. Ce n’est pas un bug, c’est l’ordre des chaînes de filtrage — mais la surprise est désagréable quand la base concernée n’avait pas de mot de passe.

La bonne pratique tient en une phrase : ne publiez que ce qui doit être atteignable depuis l’extérieur, et préfixez par 127.0.0.1 tout ce qui n’a besoin d’être joignable que localement.

services:
  web:
    ports:
      - "80:8080"              # public : c'est voulu
  db:
    ports:
      - "127.0.0.1:5432:5432"  # local uniquement, pour un client graphique
  cache:
    expose:
      - "6379"                 # documentation seulement, rien de publie

Une remarque sur localhost, à l’origine du premier problème posé en introduction. Dans un conteneur, localhost désigne le conteneur lui-même. Une application qui cherche sa base sur localhost:5432 cherche donc un Postgres à l’intérieur d’elle-même ; la bonne adresse est le nom du service, db:5432. Et si un conteneur doit réellement joindre un service qui tourne sur l’hôte, il existe un nom dédié :

# Sur Docker Desktop (macOS, Windows), ce nom est fourni d'office
docker run --rm alpine:3.20 ping -c 1 host.docker.internal

# Sur Linux, il faut le declarer explicitement
docker run --rm --add-host=host.docker.internal:host-gateway \
  alpine:3.20 ping -c 1 host.docker.internal

Segmenter la pile par le réseau

Puisqu’un conteneur ne joint que les réseaux auxquels il appartient, les réseaux sont un vrai outil de cloisonnement. Le patron classique sépare une couche exposée d’une couche interne.

services:
  proxy:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
    networks:
      - frontend            # seul service joignable de l'exterieur

  api:
    build: .
    networks:
      - frontend            # joignable par le proxy
      - backend             # peut joindre la base

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data
    networks:
      - backend             # UNIQUEMENT le reseau interne

networks:
  frontend:
  backend:
    internal: true          # aucune route vers l'exterieur

volumes:
  db-data:

L’intérêt n’est pas théorique. Le proxy n’appartient pas à backend : même compromis, il ne peut pas ouvrir une connexion TCP vers db, parce qu’aucune route n’existe. L’option internal: true va plus loin en supprimant la route par défaut vers l’extérieur, ce qui limite fortement l’exfiltration de données depuis ce réseau.

docker compose exec proxy sh -c "nc -zv db 5432"   # doit echouer
docker compose exec api sh -c "nc -zv db 5432"     # doit reussir

Trois manières de persister, et quand utiliser chacune

TypeGéré parEmplacementUsage principal
Volume nomméDocker/var/lib/docker/volumes/...Données de production : bases, téléversements
Bind mountVousN’importe où sur l’hôteDéveloppement : code source, configuration
tmpfsDockerEn mémoire vive uniquementDonnées sensibles ou temporaires

Le volume nommé est un espace que Docker crée et gère. Vous ne connaissez pas son chemin réel, et c’est précisément l’intérêt : il est portable, indépendant de l’arborescence de l’hôte, sauvegardable par des outils Docker, et identique sur les trois systèmes.

Le bind mount monte un dossier existant de l’hôte dans le conteneur. Parfait pour le développement — vous modifiez un fichier, le conteneur le voit instantanément — et inadapté à la production, car il couple l’application à l’arborescence précise d’une machine donnée.

# Volume nomme : Docker gere l'emplacement
docker run -d --name pg1 \
  -e POSTGRES_PASSWORD=secret \
  -v pg-data:/var/lib/postgresql/data \
  postgres:16-alpine

# Bind mount : chemin ABSOLU de l'hote, obligatoirement
docker run -d --name web1 \
  -v "$(pwd)/site:/usr/share/nginx/html:ro" \
  -p 8080:80 nginx:1.27-alpine

# tmpfs : en memoire, disparait a l'arret
docker run -d --name calcul --tmpfs /tmp:rw,size=64m alpine:3.20 sleep 3600

La distinction se joue sur un caractère : -v pg-data:/chemin sans barre oblique initiale désigne un volume nommé, -v /chemin/hote:/chemin un bind mount. Une faute de frappe fait donc silencieusement basculer d’un mécanisme à l’autre. La syntaxe --mount, plus verbeuse, supprime l’ambiguïté et échoue proprement si la source n’existe pas :

docker run -d \
  --mount type=volume,source=pg-data,target=/var/lib/postgresql/data \
  --mount type=bind,source="$(pwd)/conf",target=/etc/app,readonly \
  --mount type=tmpfs,target=/tmp,tmpfs-size=64m \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

Il existe un comportement différenciant qu’il faut connaître, car il explique des situations déroutantes. Quand un volume nommé vide est monté sur un dossier non vide de l’image, Docker recopie le contenu de l’image dans le volume à la première utilisation. Un bind mount, lui, ne recopie rien : il masque purement et simplement le contenu de l’image.

# Le volume est pre-rempli avec la page par defaut de nginx
docker run -d --name v1 -v html-vol:/usr/share/nginx/html nginx:1.27-alpine
docker run --rm -v html-vol:/data alpine:3.20 ls /data   # index.html present

# Le bind mount d'un dossier vide masque tout : nginx renvoie 403
mkdir -p ./vide
docker run -d --name v2 -p 8081:80 \
  -v "$(pwd)/vide:/usr/share/nginx/html" nginx:1.27-alpine

C’est exactement pour cette raison que le fichier de surcharge de la leçon précédente ajoutait un volume anonyme - /app/node_modules : le bind mount de ./src ne devait pas masquer les dépendances installées dans l’image.

Gérer les volumes au quotidien

# Creer explicitement (facultatif : un volume inconnu est cree a la volee)
docker volume create pg-data

# Lister. Les noms en hexadecimal sont des volumes ANONYMES.
docker volume ls

# Ou vivent reellement les donnees, et qui les utilise
docker volume inspect pg-data

# Supprimer. Refuse si un conteneur l'utilise.
docker volume rm pg-data

# Supprimer tous les volumes inutilises. COMMANDE DESTRUCTRICE :
# listez d'abord, decidez ensuite.
docker volume ls -f dangling=true
docker volume prune

Deux points de vigilance. Les volumes survivent à docker rm : supprimer un conteneur ne supprime pas ses données, ce qui est voulu, mais explique l’accumulation silencieuse de volumes orphelins. Et les volumes anonymes s’entassent avec des identifiants illisibles : nommez systématiquement les vôtres.

Pour inspecter un volume sans démarrer le service qui l’utilise, le conteneur jetable est l’outil idéal :

docker run --rm -v pg-data:/data alpine:3.20 ls -la /data
docker run --rm -v pg-data:/data alpine:3.20 du -sh /data

Le problème des permissions sur les bind mounts

Sur Linux, un piège attend tout le monde une fois. Les permissions reposent sur des identifiants numériques (UID, GID), et ces numéros ne sont pas traduits entre l’hôte et le conteneur. Si le conteneur écrit en tant qu’utilisateur 1001 dans un dossier monté depuis l’hôte, les fichiers créés appartiennent à l’UID 1001 de l’hôte — un compte qui n’existe peut-être pas. Le symptôme habituel : des fichiers appartenant à root apparaissent dans votre dossier de travail, ou l’application se plaint de permission denied sur un dossier que vous voyez pourtant accessible.

# Reproduction : le conteneur ecrit en root
docker run --rm -v "$(pwd):/work" -w /work alpine:3.20 touch fichier-root
ls -l fichier-root     # appartient a root

# Solution : imposer l'UID et le GID de l'utilisateur courant
docker run --rm --user "$(id -u):$(id -g)" \
  -v "$(pwd):/work" -w /work alpine:3.20 touch fichier-moi
ls -l fichier-moi      # vous appartient
# Dans Compose, avec une variable pour rester portable
services:
  api:
    build: .
    user: "${UID:-1000}:${GID:-1000}"
    volumes:
      - ./src:/app/src

Ce problème n’existe pas sur macOS et Windows, où la couche de partage de fichiers de Docker Desktop réécrit les propriétaires. En contrepartie, ces plateformes souffrent d’un coût de performance sur les fichiers partagés : un dossier node_modules ou .git en bind mount peut ralentir un projet d’un ordre de grandeur. La parade est de garder ces dossiers dans un volume Docker et de ne monter que le code source.

Un dernier réflexe utile : montez en lecture seule tout ce que le conteneur n’a pas à modifier — configuration, jeux de données, certificats. C’est gratuit et cela supprime une classe entière d’accidents.

docker run -d -v "$(pwd)/nginx.conf:/etc/nginx/nginx.conf:ro" nginx:1.27-alpine

Sauvegarder et restaurer un volume

Un volume nommé ne se copie pas avec cp : son emplacement appartient à Docker, et sur macOS ou Windows il se trouve à l’intérieur de la VM. La méthode portable consiste à le monter dans un conteneur jetable qui écrit une archive dans un dossier de l’hôte.

# --- Sauvegarde : le volume en lecture, un dossier de l'hote en ecriture
docker run --rm \
  -v pg-data:/data:ro \
  -v "$(pwd):/sauvegarde" \
  alpine:3.20 \
  tar czf /sauvegarde/pg-data-$(date +%F).tar.gz -C /data .

# --- Restauration
docker volume create pg-data-restaure
docker run --rm \
  -v pg-data-restaure:/data \
  -v "$(pwd):/sauvegarde" \
  alpine:3.20 \
  tar xzf /sauvegarde/pg-data-2026-01-15.tar.gz -C /data

Une réserve importante pour les bases de données : copier les fichiers d’un moteur en cours d’écriture peut produire une archive incohérente. Arrêtez le conteneur avant la copie, ou — bien mieux — utilisez l’outil de sauvegarde logique du moteur, qui garantit un état cohérent :

# Sauvegarde logique de Postgres, moteur en fonctionnement
docker compose exec -T db pg_dump -U app -d appdb | gzip > appdb-$(date +%F).sql.gz

# Restauration
gunzip -c appdb-2026-01-15.sql.gz | docker compose exec -T db psql -U app -d appdb

Le -T désactive l’allocation d’un pseudo-terminal, indispensable dès qu’on redirige un flux : sans lui, l’archive produite est corrompue par les caractères de contrôle du terminal.

Réseaux et volumes dans Compose

Compose déclare les deux au niveau supérieur du fichier, ce qui donne une vue complète de la topologie en un seul endroit. La forme longue est plus verbeuse mais bien plus lisible en revue.

services:
  api:
    build: .
    networks:
      - backend
    volumes:
      - uploads:/app/uploads          # forme courte : volume nomme
      - type: bind
        source: ./config
        target: /app/config
        read_only: true
      - type: tmpfs
        target: /tmp
        tmpfs:
          size: 67108864              # 64 Mo, en octets

networks:
  backend:
    driver: bridge

volumes:
  uploads:

Rappel du piège de nommage vu à la leçon précédente : Compose préfixe les volumes par le nom du projet, donc uploads devient monprojet_uploads. Renommer le dossier du projet crée un nouveau volume vide et donne l’impression que les données ont disparu, alors qu’elles dorment sous l’ancien préfixe.

docker volume ls | grep uploads       # retrouver un volume derriere son prefixe
docker compose -p monprojet up -d     # fixer le prefixe pour ne plus en dependre

À retenir

  • Sur le réseau bridge par défaut, la résolution par nom ne fonctionne pas. Créez toujours un réseau nommé — ce que Compose fait pour vous — et joignez vos services par leur nom, jamais par une adresse IP qui changera.
  • Dans un conteneur, localhost désigne le conteneur lui-même. Pour joindre un autre service, utilisez son nom ; pour joindre l’hôte, host.docker.internal (avec --add-host=host.docker.internal:host-gateway sur Linux).
  • -p 8080:80 se lie à toutes les interfaces et court-circuite ufw sur Linux. Préfixez par 127.0.0.1 tout ce qui n’a pas à être public, et ne publiez pas une base de données par habitude.
  • Volume nommé pour les données qui doivent durer, bind mount pour le code en développement, tmpfs pour ce qui ne doit jamais toucher le disque. Un volume nommé vide hérite du contenu de l’image ; un bind mount le masque.
  • Les volumes survivent à la suppression des conteneurs. docker compose down les conserve, docker compose down -v les détruit : la différence entre les deux, c’est vos données.
  • Sur Linux, les UID ne sont pas traduits à travers un bind mount. --user "$(id -u):$(id -g)" évite les fichiers appartenant à root dans votre dossier de travail.
  • Sauvegardez un volume via un conteneur jetable qui produit une archive ; pour une base de données, préférez toujours un export logique (pg_dump) à une copie de fichiers à chaud.

Conseil : en cas de doute sur une connexion entre conteneurs, docker network inspect <reseau> affiche la liste exacte des conteneurs attachés. Neuf fois sur dix, le service qui « ne répond pas » n’est simplement pas sur le même réseau.

Commentaires

Les commentaires sont alimentés par GitHub Discussions

Connectez-vous avec GitHub pour participer à la discussion

Lien copié !