Guide pratique des réseaux Docker personnalisés et de la communication entre conteneurs
Ce guide propose une exploration pratique des réseaux ponts Docker personnalisés et de leur rôle dans la communication entre conteneurs. Apprenez à créer, gérer et connecter des conteneurs à l'aide de la CLI Docker et de Docker Compose. Découvrez comment les réseaux personnalisés permettent la résolution DNS automatique, améliorent l'isolation et simplifient la communication inter-services, conduisant à des applications conteneurisées plus robustes et évolutives.
Guide pratique des réseaux Docker personnalisés et de la communication entre conteneurs
Les réseaux Docker personnalisés font partie de ces fonctionnalités qui semblent optionnelles jusqu'à ce que vous exécutiez plus d'un conteneur. Le pont par défaut peut suffire pour un test rapide, mais un pont défini par l'utilisateur vous donne des noms de service prévisibles, une isolation plus propre et un débogage plus facile. Pour une petite application avec un conteneur web, un conteneur API et une base de données, la différence est immédiate : l'API peut se connecter à db:5432 au lieu de chercher l'IP que Docker a attribuée aujourd'hui.
Ce guide se concentre sur les réseaux ponts définis par l'utilisateur sur un seul hôte Docker. Les réseaux overlay, la mise en réseau Kubernetes et la découverte de services Swarm résolvent des problèmes connexes dans les configurations multi-hôtes, mais le réseau pont reste l'outil quotidien pour le développement local, les petits déploiements et les projets Docker Compose.
Pourquoi le pont par défaut devient gênant
Docker crée automatiquement un réseau nommé bridge. Si vous exécutez des conteneurs sans spécifier de réseau, ils atterrissent généralement là. Cela fonctionne pour des cas simples, mais ce n'est pas agréable pour les applications multi-conteneurs.
Sur un réseau pont défini par l'utilisateur, Docker fournit un DNS intégré pour les noms de conteneurs et les noms de services Compose. Sur le pont par défaut, la découverte basée sur les noms est limitée et la liaison héritée n'est pas un modèle sur lequel vous devriez vous appuyer. Le résultat pratique est que les réseaux personnalisés vous permettent de configurer des applications avec des noms d'hôte stables :
DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000
C'est plus facile à lire, plus facile à déplacer entre machines et moins fragile que les adresses IP des conteneurs.
Les réseaux personnalisés créent également une frontière plus claire. Les conteneurs attachés au même réseau peuvent communiquer entre eux. Les conteneurs sur des réseaux différents ne le peuvent pas, sauf si vous attachez un conteneur aux deux ou publiez des ports via l'hôte. Ce n'est pas un modèle de sécurité complet, mais c'est une couche de séparation utile.
Créer un réseau avec la CLI Docker
Créez un pont défini par l'utilisateur :
docker network create app-net
Lancez deux conteneurs dessus :
docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer
Depuis le conteneur adminer, le nom d'hôte de la base de données est db. Vous n'avez pas besoin de connaître son IP.
Inspectez le réseau :
docker network inspect app-net
Vous verrez le pilote, le sous-réseau, la passerelle et les conteneurs connectés. Lors du débogage, cette commande répond à une question de base : les deux conteneurs sont-ils réellement sur le même réseau ?
Vous pouvez attacher un conteneur existant :
docker network connect app-net some-container
Et le détacher :
docker network disconnect app-net some-container
Docker ne supprimera pas un réseau tant que des conteneurs y sont attachés. Déconnectez ou supprimez d'abord les conteneurs :
docker network rm app-net
Les ports publiés sont différents des ports de conteneur à conteneur
Une confusion courante : les conteneurs sur le même réseau Docker n'ont pas besoin de ports hôtes publiés pour communiquer entre eux. Les ports publiés sont destinés au trafic entrant depuis l'hôte ou depuis l'extérieur de l'hôte.
Si un conteneur API écoute sur le port 3000 et qu'un conteneur web est sur le même réseau, le conteneur web peut appeler :
http://api:3000
Vous n'avez besoin de -p 3000:3000 que si vous voulez atteindre l'API depuis le navigateur de votre ordinateur portable ou depuis un autre hôte via l'hôte Docker.
Cela signifie que votre base de données ne devrait généralement pas publier de port hôte dans une configuration de type production, sauf si quelque chose en dehors de Docker a besoin d'un accès direct. Laissez l'API atteindre db:5432 via le réseau Docker privé à la place.
Utiliser Compose pour les applications multi-services normales
Docker Compose crée un réseau par défaut pour le projet même si vous n'en définissez pas. Les services peuvent se joindre par nom de service :
services:
web:
image: nginx:latest
ports:
- "8080:80"
depends_on:
- api
api:
image: my-api:latest
environment:
DATABASE_HOST: db
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
Dans ce fichier, api peut atteindre db en utilisant le nom d'hôte db. web peut atteindre api en utilisant le nom d'hôte api, en supposant que la configuration au niveau de l'application pointe là.
Vous pouvez également définir des réseaux nommés lorsque vous voulez une intention ou une séparation plus claire :
services:
web:
image: nginx:latest
ports:
- "8080:80"
networks:
- frontend
api:
image: my-api:latest
environment:
DATABASE_HOST: db
networks:
- frontend
- backend
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: devpass
networks:
- backend
networks:
frontend:
driver: bridge
backend:
driver: bridge
Ici, web ne peut pas parler directement à db car ils ne partagent pas de réseau. api est le pont entre les deux couches applicatives. C'est une forme utile pour les services réels : n'exposez que le service périphérique à l'hôte, gardez la base de données privée, et attachez chaque service uniquement là où il doit communiquer.
depends_on n'est pas la disponibilité
depends_on de Compose contrôle l'ordre de démarrage dans l'utilisation courante de Compose, mais il ne garantit pas que la base de données est prête à accepter des connexions. Votre API peut démarrer après le démarrage du processus du conteneur db et échouer car PostgreSQL est en cours d'initialisation.
Gérez la disponibilité dans l'application avec des tentatives, ou utilisez une vérification de santé et une configuration Compose qui respecte la santé du service pour votre version de Compose et votre flux de travail. Même dans ce cas, la logique de tentative au niveau de l'application reste l'habitude la plus fiable car les bases de données peuvent redémarrer après le démarrage initial.
Une configuration API pratique utilise DATABASE_HOST=db et réessaie la connexion pendant une courte période avant de se terminer avec une erreur claire.
Les sous-réseaux personnalisés sont utiles, mais ne les utilisez pas trop
Vous pouvez choisir un sous-réseau :
docker network create --subnet 172.28.0.0/16 app-net
Dans Compose :
networks:
backend:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
Cela aide lorsque le sous-réseau automatique de Docker chevauche un VPN, un réseau de bureau ou une autre route sur l'hôte. Ce n'est pas nécessaire pour la plupart des projets. Le codage en dur des IP de conteneurs devrait être rare ; les noms de service sont généralement le meilleur contrat.
Dépanner la communication réseau
Lorsqu'un conteneur ne peut pas en joindre un autre, vérifiez dans cet ordre :
- Les deux conteneurs sont-ils en cours d'exécution ?
- Sont-ils attachés au même réseau ?
- Le client utilise-t-il le nom du service/conteneur, pas
localhost? - Le serveur écoute-t-il sur le port et l'interface attendus ?
- Le port est-il publié uniquement lorsque l'accès à l'hôte est nécessaire ?
L'erreur localhost est particulièrement courante. À l'intérieur d'un conteneur, localhost signifie ce même conteneur, pas l'hôte Docker ni un autre service. Si l'API essaie de se connecter à localhost:5432, elle cherche PostgreSQL à l'intérieur du conteneur API. Utilisez db:5432 lorsque le service de base de données est nommé db.
Inspectez les réseaux :
docker network inspect app-net
Exécutez un conteneur de diagnostic temporaire sur le même réseau :
docker run --rm -it --network app-net alpine sh
À l'intérieur, installez ou utilisez les outils disponibles selon les besoins :
getent hosts db
nc -vz db 5432
Les images minimales peuvent ne pas avoir nc, curl ou des outils DNS installés. Un conteneur de débogage de courte durée est souvent plus propre que d'ajouter des paquets de dépannage à votre image d'application.
Un modèle par défaut sensé
Pour la plupart des applications mono-hôte, utilisez Compose et laissez-le créer le réseau du projet. Ajoutez des réseaux explicites lorsque vous avez besoin de séparation, comme frontend et backend. Utilisez les noms de service pour le trafic interne. Publiez uniquement les ports que les humains, les proxys inverses ou les systèmes externes doivent atteindre.
Cela vous donne une configuration facile à expliquer :
- Le navigateur atteint
localhost:8080carwebpublie un port. webatteintapivia le réseau Docker.apiatteintdbvia le réseau backend.dbn'a pas de port hôte sauf s'il y a une raison opérationnelle réelle.
Les réseaux Docker personnalisés ne sont pas seulement une fonctionnalité intéressante. Ils font la différence entre des conteneurs qui se trouvent sur la même machine et des services qui ont un modèle de communication clair.
Les alias réseau peuvent faciliter les migrations
Parfois, une application attend un nom d'hôte que vous ne voulez pas utiliser comme nom de service Compose. Vous pouvez ajouter un alias sur un réseau :
services:
postgres:
image: postgres:16
networks:
backend:
aliases:
- database
networks:
backend:
driver: bridge
Les conteneurs sur backend peuvent maintenant atteindre le service sous le nom postgres ou database. C'est pratique lors de la migration d'une application plus ancienne qui utilise déjà DATABASE_HOST=database, mais je n'utiliserais pas d'alias partout. Les noms de service sont plus simples lorsque vous contrôlez la configuration de l'application.
L'accès à l'hôte est un problème distinct
Un conteneur qui parle à un autre conteneur est différent d'un conteneur qui parle à l'hôte Docker. Sur Docker Desktop, host.docker.internal est généralement disponible. Sur Linux, la prise en charge dépend de la version et de la configuration de Docker ; de nombreuses équipes l'ajoutent explicitement lorsque nécessaire :
docker run --add-host=host.docker.internal:host-gateway ...
Utilisez-le avec parcimonie. Si un conteneur dépend fortement de services fonctionnant directement sur l'hôte, votre configuration peut devenir plus difficile à reproduire en CI ou sur la machine d'un autre développeur. Pour les bases de données et les caches, exécuter la dépendance comme un autre service sur le même réseau Docker est généralement plus propre.
Les ports internes doivent correspondre au processus, pas au commentaire du Dockerfile
La mise en réseau Docker ne se soucie pas de ce que dit une ligne EXPOSE du Dockerfile, sauf si un outil l'utilise comme métadonnée. L'application doit réellement écouter sur le port que vous appelez. Si une application Node écoute sur 3000, les autres conteneurs doivent utiliser api:3000 même si quelqu'un a écrit EXPOSE 8080 par erreur.
Vérifiez également l'adresse de liaison. Un service écoutant sur 127.0.0.1 à l'intérieur de son conteneur peut ne pas être accessible depuis d'autres conteneurs. Pour le trafic de conteneur à conteneur, le processus doit généralement écouter sur 0.0.0.0 ou l'interface réseau du conteneur.
Gardez la conception réseau ennuyeuse
Il est tentant de créer de nombreux réseaux parce que la fonctionnalité existe. Commencez par les chemins de communication dont vous avez réellement besoin. Une petite application pourrait n'avoir besoin que du réseau Compose par défaut. Une application web plus réaliste pourrait avoir besoin de frontend et backend. Au-delà, chaque nouveau réseau doit avoir une raison que quelqu'un peut expliquer lors d'un incident.
Une bonne conception réseau facilite le dépannage. Lorsque web ne peut pas atteindre db, et que vous savez qu'ils ne partagent intentionnellement pas de réseau, la réponse est architecturale plutôt que mystérieuse. Lorsque chaque service est attaché à chaque réseau, le réseau ne documente plus rien.
Une revue de passage dans le monde réel avant de livrer
Avant de considérer un script ou une configuration de conteneur comme terminé, lisez-le une fois comme si vous étiez la prochaine personne qui devra le déboguer à 2 heures du matin. Cela change ce que vous remarquez. Une invite qui avait du sens lors de l'écriture du script peut être ambiguë lorsqu'elle apparaît dans un journal CI. Un nom de service Docker qui semblait évident peut ne pas correspondre au nom de variable dans l'application. Une valeur par défaut Bash peut être sûre pour le développement et dangereuse pour la production.
J'aime faire un court essai à sec avec des valeurs délibérément gênantes. Utilisez un chemin avec des espaces. Utilisez une valeur optionnelle vide. Essayez un nom de fichier qui commence par un tiret. Exécutez le script depuis un répertoire de travail différent. Démarrez le conteneur sans une variable d'environnement attendue. Ces tests ne sont pas sophistiqués, mais ils attrapent les hypothèses qui se brisent généralement en premier.
Vérifiez également le message d'échec. Si la seule sortie est failed, les conseils de l'article n'ont pas été intégrés dans l'implémentation. Un échec utile indique quelle valeur a été utilisée, quelle vérification a échoué et ce que l'opérateur peut changer. Cela ne signifie pas vider chaque variable d'environnement ou imprimer des secrets. Cela signifie être spécifique là où la spécificité aide : le chemin de configuration, le nom de commande manquant, le nom du réseau, le nom d'hôte du service ou le port que le processus a essayé de lier.
La dernière habitude est de garder les exemples proches de la façon dont le système est réellement exécuté. Si la production utilise Compose, testez avec Compose. Si un script est lancé par systemd, testez-le avec systemd ou avec un environnement tout aussi minimal. Si une commande est censée être sûre pour le copier-coller, incluez les guillemets, les séparateurs -- et la validation dans l'exemple lui-même. Les lecteurs copient les modèles fonctionnels plus souvent qu'ils ne copient les avertissements.
Cette revue de passage n'est pas de la bureaucratie. C'est ainsi que la petite automatisation reste ennuyeuse. L'ennui est ce que vous voulez des invites shell, des chargeurs de configuration, de l'expansion de variables, des diagnostics de conteneurs et de la mise en réseau Docker. Moins le comportement est surprenant, plus il est facile pour le prochain opérateur de lui faire confiance.
Pour la mise en réseau Docker, documentez le chemin de trafic prévu à côté du fichier Compose ou dans le README du service. Une courte note comme web -> api:3000 -> db:5432 évite beaucoup de confusion. Cela facilite également les revues : si quelqu'un publie le port de la base de données ou attache web au réseau backend, le changement doit se justifier par rapport au chemin prévu.
Lorsque l'application grandit, revisitez la carte réseau. Les anciens alias, les ports publiés inutilisés et les services attachés à des réseaux dont ils n'ont plus besoin sont des sources silencieuses de risque opérationnel.