Gluetun : accéder à l’hôte et aux autres conteneurs
Un conteneur derrière Gluetun n’a pas d’interface propre. Publiez ses ports sur Gluetun et autorisez uniquement les sous-réseaux à joindre hors du tunnel.
Ce qui se passe lorsqu’un conteneur rejoint le réseau de gluetun
Un conteneur qui définit network_mode: service:gluetun n’a aucune interface réseau propre. Il rejoint l’espace de noms réseau de gluetun. La publication des ports et les règles de pare-feu ne sont donc plus des propriétés de ce conteneur, mais celles du service gluetun. Toutes les réponses ci-dessous découlent de ce fait.
Un espace de noms réseau est une copie privée de la pile réseau du noyau. Il possède ses propres interfaces, sa propre table de routage, ses propres règles de pare-feu et ses propres sockets en écoute. Docker en attribue un à chaque conteneur par défaut. Lorsque vous écrivez network_mode: service:gluetun, Docker ignore cette étape et place le nouveau conteneur dans l’espace de noms que gluetun possède déjà. Le conteneur conserve son propre système de fichiers et son propre fichier /etc/hosts. Ce deuxième élément devient important plus loin.
Vous pouvez le vérifier directement.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentCette commande affiche container:, suivi de l’identifiant du conteneur gluetun, alors qu’un conteneur normal afficherait bridge. Ce guide reprend là où s’arrête le routage du trafic Docker via un VPN avec gluetun : le tunnel fonctionne, mais plus rien ne peut communiquer avec le conteneur.
Publiez le port sur gluetun, pas sur l’application
Laissez un bloc ports: sur le service qui définit network_mode, et Docker refuse de créer le conteneur :
Error response from daemon: conflicting options: port publishing and the container type network modeLa raison est directe. Publier un port consiste à ajouter une règle NAT (network address translation) qui redirige un port de l’hôte vers l’espace de noms réseau propre au conteneur. Or ce conteneur n’en possède pas. Déplacez le mapping vers le service gluetun. Le numéro de port ne change pas, car l’application continue d’écouter sur ce port dans l’espace de noms partagé.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereUn bloc expose: sur le service dépendant ne sert également à rien, tandis qu’un bloc networks: à cet endroit bloque complètement le démarrage : Compose indique que le service déclare simultanément network_mode et networks, qui sont mutuellement exclusifs, puis refuse de charger le fichier.
Une conséquence apparaît plus tard. Tous les conteneurs de l’espace de noms partagent un seul espace de ports. Deux applications qui utilisent toutes les deux 8080 par défaut entrent donc en conflit, et la seconde à démarrer échoue avec une erreur indiquant que l’adresse est déjà utilisée. Modifiez le port de l’une d’elles dans sa propre configuration, par exemple la variable WEBUI_PORT de l’image qBittorrent de LinuxServer, puis publiez le nouveau numéro sur gluetun.
Comment les conteneurs derrière Gluetun communiquent-ils entre eux ?
Dans le namespace, ils partagent déjà une interface loopback. Un conteneur derrière Gluetun atteint son conteneur associé à 127.0.0.1:<port> sans utiliser de réseau Docker.
Depuis l’extérieur du namespace, le conteneur n’a pas de nom. Le DNS intégré de Docker résout le nom d’un service vers l’adresse de ce service sur un réseau défini par l’utilisateur. Or ce conteneur n’a d’adresse sur aucun réseau. Un conteneur standard comme Sonarr n’atteint donc pas le client torrent à http://qbittorrent:8080. Il l’atteint à http://gluetun:8080, car le socket écoute dans le namespace de Gluetun, sur l’adresse de Gluetun. Cela surprend les personnes qui savent comment fonctionnent les réseaux et les noms de services de Docker Compose et qui s’attendent à ce que la résolution habituelle s’applique. Cela fonctionne également sans publier de port sur l’hôte, car les deux conteneurs se trouvent sur le même réseau Compose.
Vérifiez le DNS avant de rechercher une autre cause. Gluetun utilise son propre resolver et réécrit /etc/resolv.conf dans son propre conteneur. En revanche, /etc/resolv.conf est un fichier propre à chaque conteneur. Celui que Gluetun a écrit n’est donc pas celui que votre application lit.
docker exec qbittorrent cat /etc/resolv.confComment accéder à un service exécuté sur l’hôte Docker ?
Utilisez host.docker.internal. Cette configuration nécessite deux réglages à deux endroits différents, car deux problèmes distincts sont en cause.
Commencez par le nom. /etc/hosts s’applique à chaque conteneur. L’entrée extra_hosts doit donc être ajoutée au conteneur de l’application, et non à gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway est une valeur spéciale que Docker remplace par une adresse interne de l’hôte lui-même. Dans une installation Docker Linux classique, il s’agit de l’adresse du bridge docker0, généralement 172.17.0.1. Vérifiez la vôtre avec ip -4 addr show docker0 sur le VPS. Docker Desktop résout ce nom automatiquement. C’est pourquoi les guides rédigés pour un ordinateur portable omettent la ligne extra_hosts, alors que le même fichier échoue sur un serveur.
Ajoutez ensuite la route. L’ajout du nom indique seulement au conteneur quelle adresse utiliser. Le paquet sort toujours par la route par défaut de gluetun, c’est-à-dire le tunnel, et le firewall de gluetun le bloque. Le symptôme est une connexion qui reste bloquée, puis expire, et non une connexion refusée. Un refus signifie que le paquet est arrivé et qu’un service a répondu négativement. Un timeout signifie qu’il n’est jamais arrivé.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Vérifiez ensuite que le service de l’hôte écoute réellement sur cette adresse. Un serveur PostgreSQL lié uniquement à 127.0.0.1 est inaccessible depuis n’importe quel conteneur, tunnel ou non, car 127.0.0.1 dans le namespace correspond à la loopback de ce namespace. Liez-le plutôt à 172.17.0.1 : il acceptera les connexions des conteneurs sans écouter sur l’interface publique. Vérifiez avec ss -lntp | grep 5432 sur l’hôte.
Ce que modifie réellement FIREWALL_OUTBOUND_SUBNETS
La documentation de gluetun décrit cette variable comme la liste, séparée par des virgules, des subnets auxquels gluetun et les conteneurs qui partagent sa network stack sont autorisés à accéder. Elle précise également que cette configuration modifie le firewall et le routage. Ces deux aspects sont importants. Gluetun ajoute une route pour chaque subnet indiqué via la gateway du bridge Docker. Les paquets destinés à ces adresses sortent donc par eth0 au lieu de passer par le tunnel. Gluetun ouvre également le firewall pour ces destinations, car il bloque sinon le trafic sortant qui ne se dirige pas vers le serveur VPN.
Indiquez la valeur sans espace après les virgules.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32Deux propriétés sont faciles à manquer. Il s’agit d’un paramètre au niveau du namespace. Il s’applique donc à tous les conteneurs situés derrière gluetun, et pas uniquement à celui auquel vous pensiez. De plus, il concerne uniquement le trafic sortant : il contrôle les connexions initiées par un conteneur. Les connexions qui arrivent sur un port publié suivent un autre chemin et ne nécessitent aucune entrée ici.
Accéder à l’interface web depuis un pair Tailscale
Tailscale attribue à chaque machine une adresse dans 100.64.0.0/10, la plage réservée au NAT de qualité opérateur. Les deux directions nécessitent des configurations différentes.
Le trafic entrant est simple. La publication de 8080:8080 sur gluetun lie ce port à toutes les adresses de l’hôte. L’interface tailscale0 de l’hôte en fait partie. Un pair ouvre donc http://<machine-name>:8080 et atteint le conteneur. Gluetun n’intervient pas dans ce chemin, car la règle NAT de Docker se trouve sur l’hôte, en dehors du namespace.
Pour rendre l’interface accessible uniquement via le tailnet, liez le port publié à l’adresse Tailscale de l’hôte plutôt qu’à toutes ses adresses.
ports:
- "100.101.102.103:8080:8080/tcp"Trouvez cette adresse avec tailscale ip -4 sur l’hôte. Ici, la liaison constitue un contrôle plus strict qu’une règle de pare-feu, car le port n’est jamais ouvert sur l’interface publique. Cela évite également le problème décrit dans Publier directement les ports Docker malgré ufw.
Le trafic sortant est le point où FIREWALL_OUTBOUND_SUBNETS intervient à nouveau. Si un conteneur doit contacter un pair, ajoutez l’adresse de ce pair. Préférez un /32 par pair plutôt que l’ensemble de /10. Les noms MagicDNS ne sont pas résolus dans le conteneur, car celui-ci n’utilise pas le resolver de l’hôte. Utilisez donc l’adresse numérique 100.x ou définissez-la avec une ligne extra_hosts. Cela s’applique également lorsque vous exécutez votre propre serveur de contrôle Tailscale avec Headscale.
Un fichier Compose complet pour cette architecture courante
Un client de téléchargement derrière le VPN, deux interfaces web accessibles uniquement sur le tailnet, et un conteneur qui lit une base de données PostgreSQL exécutée sur l’hôte.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedLisez le fichier pour comprendre le modèle, plutôt que de vous attarder sur les noms des produits. Les deux interfaces sont publiées sur gluetun et liées à l’adresse tailnet de l’hôte. Elles répondent donc sur Tailscale, et nulle part ailleurs. Seul Prowlarr contient la ligne extra_hosts, car c’est le conteneur Prowlarr qui résout les noms host.docker.internal. FIREWALL_OUTBOUND_SUBNETS définit deux adresses uniques : l’adresse du bridge Docker de l’hôte, pour que Prowlarr puisse ouvrir une connexion à la base de données, et celle d’un pair du tailnet.
Le serveur PostgreSQL est volontairement absent du fichier. Il s’exécute sur le VPS comme un service système classique et écoute sur 172.17.0.1:5432. C’est la même architecture que une stack arr avec Docker Compose, avec la base de données déplacée en dehors de Docker.
Conservez la clé privée WireGuard en dehors du fichier Compose. ${WIREGUARD_PRIVATE_KEY} la lit depuis un fichier .env situé à côté de celui-ci. Ce modèle est présenté dans les fichiers env et les secrets pour Docker Compose. La clause condition: service_healthy utilise le healthcheck déjà fourni par l’image gluetun. Ainsi, aucun conteneur ne démarre tant que le tunnel n’est pas signalé comme actif. Les healthchecks de Compose expliquent la forme générale.
Publier sur toutes les adresses au lieu du seul tailnet
Supprimez le préfixe d’adresse et les bindings de ports sur 0.0.0.0, ce qui inclut l’adresse IP publique du VPS. Faites-le uniquement derrière un firewall que vous contrôlez, et lisez d’abord la note sur ufw ci-dessus.
ports:
- "8080:8080/tcp"Vérifiez que le tunnel achemine toujours le trafic
Exécutez la même requête deux fois, une fois depuis le namespace et une fois depuis l’hôte, puis comparez les résultats.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgLa première commande doit afficher l’adresse de sortie de votre fournisseur VPN. La seconde doit afficher l’adresse du VPS. Si elles sont identiques, le trafic du conteneur ne passe pas par le tunnel. Tant que ce problème n’est pas corrigé, les autres corrections de ce guide sont sans effet.
La table de routage indique quel trafic passe par le tunnel et quel trafic ne l’emprunte pas.
docker run --rm --network=container:gluetun alpine:3.22 ip route showLa route par défaut doit pointer vers l’interface du tunnel, tun0. Vous devez ensuite voir une route par entrée de FIREWALL_OUTBOUND_SUBNETS, qui pointe vers la passerelle du bridge Docker. Toute autre route passant par eth0 correspond à du trafic qui contourne le VPN.
Le control server de Gluetun indique la même adresse IP publique sur le port 8000, à l’adresse /v1/publicip/ip. Les versions récentes exigent de configurer l’authentification des routes du control server. Configurez-la avant de vous appuyer sur ce mécanisme.
La fuite créée par un mauvais sous-réseau
FIREWALL_OUTBOUND_SUBNETS est une ouverture volontaire dans le pare-feu. La taille de cette ouverture détermine donc l’ampleur du risque. Voici quatre façons de la rendre trop large :
0.0.0.0/0envoie tout le trafic hors du tunnel. Les deux vérifications d’adresse IP ci-dessus le détectent dès le premier lancement, car elles renvoient la même adresse.- Une plage plus large que nécessaire. Ouvrir
10.0.0.0/8pour atteindre une machine à l’adresse10.0.1.7ouvre également toutes les adresses qu’un pair du torrent peut annoncer dans cette plage. Écrivez10.0.1.7/32. - Une plage qui chevauche les adresses propres au tunnel. La documentation de gluetun indique que gluetun envoie alors le trafic VPN via le bridge au lieu du tunnel, ce qui casse la redirection de port. Vérifiez la valeur
WIREGUARD_ADDRESSESavant d’ouvrir une plage privée. 100.64.0.0/10pour Tailscale. Cela ouvre environ quatre millions d’adresses pour atteindre un seul pair. Déclarez les pairs nécessaires avec des entrées/32.
N’oubliez pas que ce paramètre s’applique à tout le namespace. Ouvrir un sous-réseau pour qu’un indexer puisse atteindre un service de l’hôte ouvre le même sous-réseau au client torrent qui partage ce namespace. Relancez la vérification de l’adresse IP publique après chaque modification de cette variable, car c’est le seul test qui indique si la modification a produit le résultat attendu.
Ce qui casse lorsque vous redémarrez gluetun
gluetun possède le namespace. Le cycle de vie de gluetun est donc celui du namespace. Le démarrage d’un conteneur dépendant échoue immédiatement lorsque gluetun est arrêté :
Error response from daemon: cannot join network of a non running containerRedémarrer gluetun sur place provoque une panne moins visible. Les conteneurs dépendants continuent de fonctionner alors que le namespace auquel ils étaient attachés est recréé sous leurs pieds. Ainsi, docker ps indique que tout fonctionne et aucun service ne répond. Après toute modification du service gluetun, recréez l’ensemble du groupe au lieu de redémarrer un seul de ses éléments.
docker compose up -d --force-recreateLa même règle s’applique aux mises à jour de l’image. Si vous téléchargez une nouvelle image gluetun et recréez uniquement ce service, les autres conteneurs restent attachés à un namespace qui n’existe plus.
FAQ
Pourquoi Docker affiche-t-il « port publishing and the container type network mode » ?
Parce qu’un bloc ports: est encore présent sur un service qui définit également network_mode: service:gluetun. La publication d’un port ajoute une règle NAT qui redirige un port de l’hôte vers l’espace de noms réseau propre à un conteneur. Or un conteneur utilisant ce mode n’en possède pas. Supprimez le bloc ports: de ce service et ajoutez le même mapping au service gluetun. Le numéro de port reste identique, car l’application continue à écouter sur ce port dans l’espace de noms partagé.
Comment les autres conteneurs accèdent-ils à un service placé derrière gluetun ?
Les conteneurs qui utilisent le même espace de noms communiquent entre eux via 127.0.0.1. Les conteneurs situés en dehors de cet espace utilisent le nom du service gluetun. http://gluetun:8080 fonctionne donc, contrairement à http://qbittorrent:8080. Le conteneur de l’application ne possède aucune adresse sur un réseau Docker. Le serveur DNS intégré n’a donc rien à résoudre pour son nom. Aucune publication de port n’est nécessaire dans ce cas, à condition que les deux conteneurs partagent un réseau Compose.
Que dois-je mettre dans FIREWALL_OUTBOUND_SUBNETS ?
Uniquement les adresses auxquelles un conteneur situé derrière gluetun doit se connecter, en utilisant la plage la plus précise possible. Une seule machine s’écrit /32. Les deux entrées courantes sont l’hôte Docker, indiqué par 172.17.0.1/32, et un /32 pour chaque pair Tailscale auquel vous vous connectez. N’ajoutez jamais 0.0.0.0/0. N’ajoutez pas non plus de plage qui chevauche les adresses du tunnel de votre VPN. Les connexions entrantes vers un port publié ne nécessitent aucune entrée ici.
Pourquoi le conteneur ne peut-il pas résoudre mes noms MagicDNS Tailscale ?
MagicDNS fonctionne en configurant le resolver de l’hôte pour utiliser le serveur DNS de Tailscale. Le conteneur n’utilise pas le resolver de l’hôte. Il utilise la configuration indiquée par son propre /etc/resolv.conf. Derrière gluetun, il s’agit de la configuration DNS de gluetun. Vérifiez-la avec docker exec <container> cat /etc/resolv.conf. Utilisez l’adresse numérique 100.x du pair ou associez le nom à cette adresse avec une entrée extra_hosts dans ce conteneur.
Comment vérifier que le trafic passe toujours par le VPN ?
Exécutez une requête depuis l’espace de noms, puis la même requête depuis l’hôte, et comparez les réponses. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org doit renvoyer l’adresse de sortie de votre fournisseur VPN, tandis que curl -s https://api.ipify.org, exécuté sur le VPS, doit renvoyer l’adresse du VPS. Deux réponses identiques indiquent que le tunnel ne transporte pas le trafic du conteneur. Recommencez cette vérification après chaque modification de FIREWALL_OUTBOUND_SUBNETS.