Réseau Docker Compose : DNS, ports et mode host
Comprenez le réseau bridge par défaut, le DNS par nom de service, le mode host, le partage entre projets et le port publié qui contourne UFW.
Ce que Compose crée avant le démarrage de votre application
Le réseau Docker Compose repose sur une règle simple : docker compose up crée un réseau privé pour le projet, y rattache tous les services et permet à ces services de communiquer entre eux par nom de service. Vous n'avez pas besoin d'écrire une seule ligne networks: pour cela. La plupart des confusions liées au réseau Compose viennent du fait que ce réseau par défaut existe déjà.
Voici un fichier simple. Enregistrez-le sous compose.yaml dans un répertoire appelé shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleDémarrez-le et examinez ce que Docker a créé :
docker compose up -d
docker network lsLa liste contient maintenant un réseau appelé shop_default. Compose le nomme <project>_default, et le nom du projet correspond par défaut au nom du répertoire en minuscules. Remplacez cette valeur avec docker compose -p myproject up -d ou avec un name: myproject de niveau supérieur dans le fichier. Son driver est bridge, qui est un switch virtuel à l'intérieur de l'hôte. Chaque conteneur reçoit une adresse sur un sous-réseau privé, et le trafic sortant est traduit vers l'adresse de l'hôte lors de sa sortie.
docker compose down supprime de nouveau ce réseau. C'est pourquoi un ancien conteneur d'un projet précédent peut maintenir un réseau ouvert : Docker renvoie error while removing network: network shop_default has active endpoints, et il faut arrêter ou supprimer le conteneur qui y est encore rattaché.
Si Compose est nouveau pour vous, consultez d'abord la structure du fichier Compose et les commandes de cycle de vie, car tout ce qui suit suppose que vous savez démarrer et arrêter un projet.
Le DNS par nom de service est le point que les débutants manquent
Sur tout réseau défini par l’utilisateur, Docker exécute un serveur DNS intégré que chaque conteneur voit à l’adresse 127.0.0.11. Il résout les noms de service vers les adresses actuelles des conteneurs. Ainsi, web atteint la base de données sur le nom d’hôte db et le port 5432, sans aucune configuration.
docker compose exec web getent hosts dbCette commande affiche une ligne semblable à 172.18.0.2 db. Si elle n’affiche rien, les deux services ne sont pas sur le même réseau.
L’erreur que presque tout le monde commet au moins une fois consiste à utiliser localhost dans la configuration de l’application. Dans un conteneur, localhost désigne ce conteneur, et non l’hôte ni l’autre service. Les clients Postgres le signalent clairement :
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?La chaîne de connexion doit être postgresql://postgres:example@db:5432/postgres. La partie hôte est le nom du service.
Deux détails vous feront gagner du temps par la suite. Les noms sont résolus vers ce qui est actuellement en cours d’exécution. Ainsi, docker compose up -d --scale web=3 fournit un nom avec trois adresses, tandis qu’un client qui met le DNS en cache indéfiniment se retrouvera associé à un conteneur arrêté. De plus, le réseau historique bridge utilisé par un docker run simple, sans --network, n’offre aucune résolution de noms. C’est pourquoi les conseils de 2016 sur les liens entre conteneurs ne correspondent pas à ce que vous observez.
Vous n’avez pas besoin de ports: pour connecter deux services
ports: publie un port de conteneur sur l’hôte. Il sert au trafic entrant depuis l’extérieur de Docker. Il n’a aucun rapport avec le trafic entre services, qui fonctionne déjà sur toute la plage de ports du réseau du projet.
La ports: - "5432:5432" que beaucoup de personnes ajoutent à leur service de base de données est donc inutile et présente un risque réel : elle expose Postgres sur l’interface publique du serveur. Supprimez-la. Si vous devez y accéder depuis votre ordinateur portable pour une migration, liez-la à loopback avec "127.0.0.1:5432:5432" et utilisez un tunnel SSH. La différence entre un listening socket, un port publié et une règle de pare-feu est expliquée dans le fonctionnement des ports et des services en écoute sous Linux.
expose: sert uniquement de documentation avec Compose. Elle n’ouvre aucun accès, car rien n’était fermé entre les conteneurs du même réseau.
Quand network_mode: host est approprié, et ce qu’il implique
Le mode host supprime le namespace réseau propre au conteneur et permet au processus d’utiliser directement les interfaces de l’hôte.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityCe mode peut être nécessaire dans certains cas. Un processus qui doit voir le trafic broadcast ou multicast du réseau local, par exemple pour la découverte d’appareils d’un media server ou d’une hub domotique, ne peut pas le voir derrière un bridge, car celui-ci ne transfère pas ce trafic au conteneur. Un agent de supervision qui lit les compteurs des interfaces de l’hôte doit utiliser les interfaces de l’hôte. Vous supprimez également l’étape de translation d’adresses, ce qui peut être important avec des débits élevés de paquets.
Les inconvénients sont spécifiques.
ports: ne fonctionne plus. Docker avertit que les ports publiés sont ignorés en mode réseau host, et que le conteneur utilise les ports auxquels son processus se lie. Deux conteneurs en mode host qui veulent utiliser le port 8080 entrent en conflit, et le second s’arrête avec bind: address already in use.
La résolution des noms de services disparaît dans les deux sens. Le conteneur ne se trouve pas sur le réseau du projet. Il ne peut donc pas résoudre db, et les autres services ne peuvent pas le résoudre non plus. Il ne peut les atteindre que via les ports publiés sur l’hôte, généralement sur 127.0.0.1.
L’isolation disparaît. Un processus qui se lie à 0.0.0.0 dans un conteneur en mode host écoute sur toutes les interfaces de votre serveur, y compris l’interface publique, exactement comme un paquet installé avec apt. Ce mode a toutefois un avantage : ce trafic suit le chemin d’entrée normal, donc les règles UFW s’appliquent, contrairement aux ports publiés.
Le mode host est une fonctionnalité de Docker Engine sous Linux. Docker Desktop ne le prend en charge qu’à partir de la version 4.34, et uniquement après son activation. De plus, les conteneurs ne peuvent pas se lier aux adresses IP de l’hôte, et seuls TCP et UDP sont pris en charge. Si une partie de votre équipe utilise des serveurs Linux et l’autre Docker Desktop, attendez-vous à ce que le même fichier se comporte différemment.
Utilisez le mode host lorsque vous avez besoin des interfaces de l’hôte. Ne l’utilisez pas pour résoudre un problème de connexion, car il remplace généralement un problème par un autre, plus difficile à résoudre.
Connecter deux projets Compose avec un réseau externe
Un réseau créé par un projet n’est pas visible par un autre. C’est pourquoi un reverse proxy dans proxy/compose.yaml ne peut pas voir une application dans app/compose.yaml, même sur le même serveur. La solution consiste à utiliser un réseau dont aucun des deux projets n’est propriétaire.
Créez-le une seule fois, manuellement :
docker network create edgeDéclarez-le ensuite comme externe dans chaque projet. Côté proxy :
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueCôté application :
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true indique à Compose de se connecter à un réseau existant au lieu d’en créer un, et de le laisser en place sur docker compose down. La clé distincte name: est plus importante qu’il n’y paraît : sans elle, Compose recherche un réseau nommé exactement edge. Elle permet de donner un nom au réseau dans votre fichier et un autre nom sur l’hôte.
Si le réseau n’existe pas, Compose refuse de démarrer et indique que le réseau a été déclaré comme externe, mais qu’il est introuvable. Créez-le d’abord.
Notez ce que fait le fichier de l’application avec internal. La base de données se trouve uniquement sur ce réseau local au projet. Le proxy ne peut donc pas l’atteindre, et seul app le peut. Ajouter internal: true sous un réseau va plus loin et supprime complètement sa route vers l’extérieur. C’est une bonne valeur par défaut pour une base de données, mais vous devez connaître un inconvénient avant de l’utiliser : un conteneur sur un réseau interne ne peut rien télécharger. Un entrypoint qui exécute apt-get update ou pip install au démarrage reste donc bloqué, puis échoue avec un délai d’expiration.
Pour consulter une configuration complète avec les règles de routage et les certificats, voir exécuter plusieurs applications derrière une seule instance Traefik.
Les ports publiés contournent UFW
C'est la partie du réseau Compose qui peut entraîner un incident de sécurité. Vous publiez un port, vous vérifiez qu'UFW est actif et refuse tout sauf SSH, mais le service reste accessible depuis Internet.
sudo ufw status
curl http://203.0.113.10:8080UFW indique que le port est bloqué. Le curl renvoie quand même la page. Rien n'est défaillant. Docker écrit directement ses propres règles de translation d'adresses et de forwarding dans iptables. Le trafic destiné à un port publié d'un conteneur est transféré vers le conteneur au lieu d'être remis à l'hôte. Il ne passe donc jamais par la chaîne gérée par UFW pour le trafic destiné localement. Les règles de Docker sont également évaluées avant celles d'UFW.
La solution rapide consiste à publier le port uniquement là où vous en avez besoin :
ports:
- "127.0.0.1:8080:80"Cela lie le côté hôte à loopback. Le port est donc accessible depuis le serveur lui-même et via un tunnel SSH, mais depuis aucun autre endroit. Placez le point d'entrée public derrière un reverse proxy qui publie volontairement les ports 80 et 443. L'explication complète, notamment la chaîne DOCKER-USER à utiliser lorsque vous devez filtrer un port publié, se trouve dans pourquoi Docker publie directement en contournant UFW et comment corriger ce problème.
Comment le déboguer avec quatre commandes
Commencez par vérifier à quel réseau chaque conteneur est réellement connecté :
docker network inspect shop_defaultLe bloc Containers répertorie tous les conteneurs connectés, avec leur adresse. Un service absent de cette liste utilise un autre réseau, le mode host ou n'est pas en cours d'exécution.
Testez la résolution de noms depuis un conteneur temporaire connecté au même réseau. Vous n'avez ainsi besoin d'aucun outil dans vos propres images :
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432Un échec de nslookup indique un problème de résolution de noms ou d'appartenance au réseau. Si nslookup réussit alors que nc échoue, le service est en cours d'exécution, mais n'écoute pas sur ce port, ou écoute sur 127.0.0.1 dans son propre conteneur au lieu de 0.0.0.0. C'est fréquent avec les serveurs de développement. La correction se trouve dans l'adresse d'écoute de l'application, pas dans Docker.
Un autre problème peut ressembler à un bug de Docker. Si les conteneurs peuvent communiquer entre eux, mais ne peuvent pas joindre une machine de votre réseau professionnel ou VPN, le sous-réseau Docker chevauche probablement ce réseau. Docker alloue par défaut des sous-réseaux à partir de 172.17.0.0/16. Modifiez le pool dans /etc/docker/daemon.json :
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}Exécutez ensuite sudo systemctl restart docker et recréez les réseaux concernés, car un réseau existant conserve le sous-réseau avec lequel il a été créé.
FAQ
Pourquoi mes conteneurs ne peuvent-ils pas se joindre par nom de service ?
Ils ne sont pas sur le même réseau. Compose place automatiquement chaque service sur <project>_default, mais dès que vous ajoutez une liste networks: à un service, cette liste devient l'ensemble complet de ses réseaux et le réseau par défaut n'est plus implicite. Exécutez docker network inspect <network> et vérifiez que les deux conteneurs apparaissent dans le bloc Containers. Vérifiez également qu'aucun des deux services n'utilise network_mode: host, car un conteneur en mode host n'est connecté à aucun réseau Docker et ne peut pas résoudre les noms de service.
Dois-je publier les ports pour qu'un service puisse joindre un autre service ?
Non. Sur un réseau Compose, les autres conteneurs de ce réseau peuvent atteindre tous les ports de chaque conteneur. ports: sert uniquement à exposer un conteneur au trafic provenant de l'extérieur de Docker, et expose: fournit de la documentation. Publier le port d'une base de données est une pratique courante et risquée, car cela expose la base de données sur l'interface publique de votre serveur.
Quelle est la différence entre les réseaux bridge et host ?
Le réseau bridge fournit au conteneur son propre espace de noms réseau et une adresse sur un switch virtuel, avec la résolution automatique des noms entre les conteneurs et la traduction du trafic sortant. Le réseau host donne directement au conteneur accès à la pile réseau de l'hôte : pas d'adresse distincte, pas de résolution par nom de service, pas de publication de ports et aucune isolation vis-à-vis des autres services en écoute sur l'hôte. Bridge est le réseau par défaut et convient dans la plupart des cas, sauf si le processus doit utiliser les interfaces de l'hôte.
Comment connecter des conteneurs issus de deux fichiers Compose différents ?
Créez un réseau partagé avec docker network create edge, puis déclarez-le dans les deux fichiers avec external: true et connectez-y les services qui doivent communiquer. Compose ne le créera ni ne le supprimera. Si vous omettez l'étape de création, Compose refuse de démarrer et indique que le réseau est déclaré comme externe, mais introuvable.
Pourquoi mon conteneur est-il accessible depuis Internet alors qu'UFW bloque le port ?
Parce qu'un port publié est géré par les règles de forwarding que Docker ajoute à iptables. Ces règles sont évaluées avant celles d'UFW, et le trafic forwardé ne passe de toute façon pas par la chaîne filtrée par UFW. Liez le côté hôte à l'interface loopback avec "127.0.0.1:8080:80" et placez tout service public derrière un reverse proxy sur les ports 80 et 443.