Gluetun : accéder à l’hôte et aux autres conteneurs
Un conteneur derrière Gluetun n’a pas d’interface réseau propre. Publiez ses ports sur Gluetun et autorisez seulement les sous-réseaux à joindre hors 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 pas ses propres interfaces réseau. Il rejoint l’espace de noms réseau de gluetun. La publication des ports et les règles de firewall ne relèvent donc plus de ce conteneur, mais 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 firewall 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, mais pas ses propres /etc/hosts ni /etc/resolv.conf. Docker lui transmet les mêmes fichiers que ceux utilisés par gluetun, ce qui aura son importance 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ù le routage du trafic Docker via un VPN avec gluetun s’arrête : le tunnel fonctionne, mais plus rien ne peut communiquer avec le conteneur.
Publier 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 cause 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 échoue de la même manière, avec conflicting options: port exposing and the container type network mode. Un bloc networks: à cet endroit bloque également le démarrage : Compose indique que le service déclare 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. La deuxième à démarrer échoue avec une erreur indiquant que l’adresse est déjà utilisée. Modifiez 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 frère à 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, mais 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. Ce comportement surprend les personnes qui savent comment fonctionnent les réseaux Docker Compose et les noms de service et qui s’attendent à retrouver le nommage habituel. Cela fonctionne également sans publier quoi que ce soit sur l’hôte, puisque les deux conteneurs sont sur le même réseau Compose.
Vérifiez le DNS avant de rechercher toute autre cause. Gluetun exécute son propre serveur DNS dans le namespace. Docker fournit à chaque conteneur derrière gluetun le même fichier /etc/resolv.conf qu’à gluetun. Les résolutions de votre application passent donc par le résolveur de gluetun, et non par celui de l’hôte. Par défaut, ce résolveur refuse également les réponses qui pointent vers des adresses privées. C’est pourquoi un nom qui se résout sur l’hôte peut échouer dans le conteneur. La documentation de gluetun indique que ces noms d’hôte doivent être ajoutés à DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES.
docker exec qbittorrent cat /etc/resolv.confComment accéder à un service exécuté sur l’hôte Docker ?
Utilisez host.docker.internal. Il faut deux réglages, tous deux sur le service gluetun, car deux éléments différents sont incorrects.
Le nom vient en premier. Un conteneur derrière gluetun lit le /etc/hosts de gluetun, et Docker rejette une entrée extra_hosts sur un service qui définit network_mode avec conflicting options: custom host-to-IP mapping and the network mode. L’entrée doit donc être définie sur gluetun, et tous les conteneurs du namespace la voient.
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. Sur une installation Docker Linux standard, il s’agit de l’adresse du bridge docker0, généralement 172.17.0.1. Confirmez la valeur utilisée sur votre VPS avec ip -4 addr show docker0. Docker Desktop résout ce nom automatiquement. C’est pourquoi les guides écrits pour un laptop omettent la ligne extra_hosts, puis le même fichier échoue sur un serveur.
La route vient ensuite. Ajouter le nom indique seulement au conteneur quelle adresse utiliser. Le paquet sort toujours par la route par défaut de gluetun, qui est le tunnel, et le pare-feu de gluetun le bloque. Le symptôme est une connexion qui attend, puis expire, et non une connexion refusée. Un refus signifie que le paquet est arrivé et qu’un service a répondu par la négative. 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 bien sur cette adresse. Un serveur PostgreSQL lié uniquement à 127.0.0.1 est inaccessible depuis tout conteneur qui possède son propre namespace réseau. Cela inclut tous les conteneurs derrière gluetun, tunnel ou non, car 127.0.0.1 dans le namespace correspond à la loopback de ce namespace. Seul un conteneur utilisant le réseau de l’hôte partage la loopback de l’hôte, et un conteneur derrière gluetun a déjà utilisé son network_mode pour gluetun. Liez plutôt PostgreSQL à 172.17.0.1 : il accepte les connexions des conteneurs tout en restant inaccessible depuis 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 sous-réseaux auxquels gluetun et les conteneurs qui partagent sa pile réseau sont autorisés à accéder. Elle précise aussi que cette variable entraîne des modifications du pare-feu et du routage. Les deux aspects sont importants. Gluetun ajoute une route pour chaque sous-réseau indiqué via la passerelle du bridge Docker. Les paquets destinés à ces adresses sortent donc par eth0 au lieu de passer par le tunnel. Gluetun ouvre également le pare-feu pour ces sous-réseaux, 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 de l’espace de noms. Il s’applique donc à tous les conteneurs placés derrière gluetun, et pas uniquement à celui auquel vous pensiez. Il concerne uniquement le trafic sortant : il contrôle les connexions qu’un conteneur établit. 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 niveau opérateur. Cela ne coûte rien sur un tailnet personnel, mais la limite de six utilisateurs du forfait gratuit détermine si cela reste vrai lorsque d’autres personnes doivent accéder aux mêmes interfaces. Le nombre d’appareils utilisateurs est illimité avec tous les forfaits. Les serveurs et les autres appareils marqués sont comptabilisés séparément : la page tarifaire de Tailscale indique que 50 ressources marquées sont incluses, puis que chaque ressource supplémentaire coûte $1 par mois. Comme la facturation compte sinon les utilisateurs plutôt que les machines, le coût réel d’un tailnet payant dépend surtout du nombre de personnes que vous invitez, et non du nombre de conteneurs que vous leur exposez. Les deux sens de communication nécessitent une configuration différente.
Le trafic entrant est le plus simple. Publier 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 peut donc ouvrir http://<machine-name>:8080 et atteindre 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, le binding offre un contrôle plus strict qu’une règle de firewall, car le port n’est jamais ouvert sur l’interface publique. Si vous préférez accéder à l’interface avec un nom HTTPS plutôt qu’avec une adresse et un port, tailscale serve peut se placer devant ce port publié. Toutefois, serve et funnel diffèrent quant aux personnes qui peuvent finalement y accéder, et un seul des deux maintient l’interface sur le tailnet. Cette solution évite également le problème décrit dans Docker qui publie directement les ports en contournant ufw. Le modèle sidecar Tailscale pour Compose va plus loin et supprime complètement le port de l’hôte. Il fonctionne toutefois en plaçant l’application dans le namespace réseau du sidecar, et un conteneur situé derrière gluetun a déjà utilisé son unique network_mode pour gluetun.
C’est pour le trafic sortant que FIREWALL_OUTBOUND_SUBNETS intervient. 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. Si la machine contactée se trouve sur un réseau privé accessible via un VPS qui annonce ce sous-réseau à votre tailnet, indiquez la plage annoncée au lieu de l’adresse 100.x du routeur lui-même. Vérifiez également que l’hôte a bien accepté ces routes. Les noms MagicDNS ne sont pas résolus par défaut dans le conteneur, car celui-ci utilise le serveur DNS de gluetun et non le resolver de l’hôte. Utilisez donc l’adresse numérique 100.x ou associez le nom à une adresse avec une ligne extra_hosts dans le service gluetun. L’option DNS_UPSTREAM_PLAIN_ADDRESSES de gluetun peut transmettre les requêtes à un autre resolver, mais elle les envoie toutes en clair. La documentation de gluetun déconseille cette option, car les requêtes vers une adresse privée sortent du tunnel VPN. Le même principe s’applique 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 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
extra_hosts:
- "host.docker.internal:host-gateway"
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"
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 pour retenir 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. La ligne extra_hosts est également placée sur gluetun, car Prowlarr lit le /etc/hosts de gluetun lorsqu’il résout host.docker.internal, et Docker refuserait cette ligne si elle était définie sur Prowlarr. FIREWALL_OUTBOUND_SUBNETS définit deux adresses uniques : l’adresse du bridge Docker de l’hôte, afin 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 en tant que service système standard 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 hors de Docker.
Conservez la clé privée WireGuard en dehors du fichier Compose. ${WIREGUARD_PRIVATE_KEY} lit un fichier .env placé à 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. Aucun service ne démarre donc tant que le tunnel ne signale pas qu’il est opérationnel. Les healthchecks de Compose expliquent la forme générale.
Publier sur toutes les adresses plutôt que sur le 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 remarque sur ufw ci-dessus.
ports:
- "8080:8080/tcp"Vérifier que le tunnel transporte toujours du trafic
Exécutez la même requête deux fois : une fois depuis le namespace et une fois depuis l’hôte. 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 doit afficher l’adresse de sortie de votre fournisseur VPN. La seconde doit afficher l’adresse du VPS. Si les deux adresses sont identiques, le trafic du conteneur ne passe pas par le tunnel. Dans ce cas, le reste de ce guide est sans objet tant que le problème n’est pas corrigé.
Les règles et les tables de routage indiquent quel trafic passe par le tunnel et quel trafic ne l’emprunte pas.
docker run --rm --network=container:gluetun alpine:3.22 sh -c 'ip rule show; ip route show table 51820; ip route show table 199'Avec la configuration WireGuard utilisée dans ce guide, gluetun nomme l’interface du tunnel wg0. Le nom tun0 est uniquement sa valeur par défaut pour OpenVPN. Gluetun conserve également la route par défaut du tunnel en dehors de la table de routage principale. ip rule show doit contenir une règle qui envoie vers la table 51820 tout le trafic qui n’est pas marqué par WireGuard lui-même. Cette table doit contenir une route par défaut sur wg0. La table 199 contient une route pour chaque entrée de FIREWALL_OUTBOUND_SUBNETS. Chacune pointe vers la gateway du bridge Docker sur eth0, et sa règle est évaluée juste avant celle du tunnel. Un simple ip route show affiche encore la route par défaut propre à Docker sur eth0. C’est attendu et il ne s’agit pas d’une fuite, car les deux règles de gluetun sont évaluées avant la table principale. Toute route de la table 199 que vous n’avez pas indiquée correspond à du trafic qui contourne le VPN.
Le control server de gluetun peut également fournir la même adresse IP publique sur le port 8000, à l’adresse /v1/publicip/ip, mais uniquement après la configuration de l’authentification par défaut. La documentation de gluetun précise que toutes les routes du control server sont désormais privées par défaut. Une requête directe vers ce chemin échoue donc. Configurez-le avec un fichier d’authentification monté dans le conteneur à /gluetun/auth/config.toml, en utilisant la méthode basic ou apikey, puis redémarrez gluetun. Le fichier Compose ci-dessus monte déjà ./gluetun sur /gluetun. Le fichier doit donc se trouver à ./gluetun/auth/config.toml sur l’hôte. La variable HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE est la méthode la plus rapide. Si vous la définissez sur {"auth":"none"}, l’authentification est entièrement désactivée, ce que la documentation déconseille fortement. Dans une release antérieure à v3.39.1, le serveur n’est pas verrouillé de cette manière. La documentation vous avertit alors de faire confiance à tout ce qui peut l’atteindre.
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 grande :
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 aussi toutes les adresses qu’un pair BitTorrent peut annoncer dans cette plage. Écrivez10.0.1.7/32. - Une plage qui chevauche les adresses du tunnel. La documentation de gluetun indique que, dans ce cas, gluetun envoie le trafic VPN via le bridge, 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 sous forme d’entrées/32.
N’oubliez pas que ce paramètre s’applique à l’ensemble du 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 BitTorrent 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 produit bien le résultat attendu.
Ce qui se passe lorsque vous redémarrez gluetun
Gluetun contrôle le namespace. Le cycle de vie de gluetun est donc celui du namespace. Le démarrage d’un conteneur dépendant alors que gluetun est arrêté échoue immédiatement :
Error response from daemon: cannot join network namespace of a non running container: container <id> is exitedRedémarrer gluetun sur place provoque une panne plus discrète. Les conteneurs dépendants continuent de fonctionner alors que le namespace auquel ils sont attachés est recréé sous-jacent. Ainsi, docker ps indique que tout fonctionne et aucune réponse n’est renvoyée. 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 des images. Télécharger une nouvelle image gluetun et recréer uniquement ce service laisse les autres conteneurs pointer vers 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 défini sur un service qui configure é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 de l’écouter 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 s’atteignent via 127.0.0.1. Ceux qui se trouvent à l’extérieur 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, tant que les deux conteneurs partagent un réseau Compose.
Que dois-je mettre dans FIREWALL_OUTBOUND_SUBNETS ?
Uniquement les adresses auxquelles un conteneur placé derrière gluetun doit établir une connexion, avec le préfixe le plus précis possible. Une seule machine correspond à un /32. Les deux entrées courantes sont l’hôte Docker à 172.17.0.1/32 et un /32 pour chaque pair Tailscale auquel vous vous connectez. N’ajoutez jamais 0.0.0.0/0 et n’ajoutez jamais une plage qui chevauche les adresses de tunnel utilisées par 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 résolveur de l’hôte pour utiliser le serveur DNS de Tailscale. Le conteneur n’utilise pas le résolveur de l’hôte. Il utilise le /etc/resolv.conf qu’il partage avec gluetun, lequel pointe vers le propre serveur DNS de gluetun. Vérifiez avec docker exec <container> cat /etc/resolv.conf. Utilisez l’adresse numérique 100.x du pair ou associez le nom à une entrée extra_hosts sur le service gluetun. Gluetun peut transférer les requêtes vers un autre résolveur avec DNS_UPSTREAM_PLAIN_ADDRESSES, mais ces requêtes sont alors envoyées en clair et sa documentation le déconseille.
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.