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 seulement 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 ne possède aucune interface réseau propre. Il rejoint le namespace réseau de gluetun. La publication des ports et les règles du 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 seul fait.
Un namespace réseau est une copie privée de la pile réseau du kernel : 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 le namespace que gluetun possède déjà. Le conteneur conserve son propre système de fichiers et son propre fichier /etc/hosts. Ce second élément 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ù 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 simple. 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 du 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 écoute toujours 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 est tout aussi inutile. Un bloc networks: à cet endroit bloque complètement le chargement : 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 même 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 accède à son conteneur associé via 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. Ce conteneur n’a d’adresse sur aucun réseau. Un conteneur standard comme Sonarr n’accède donc pas au client torrent via http://qbittorrent:8080. Il y accède via 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 service Docker Compose et qui s’attendent à ce que la résolution de noms 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 tout autre diagnostic. 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 lit votre application.
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 paramètres à deux endroits différents, car deux éléments distincts sont incorrects.
Commencez par le nom. /etc/hosts est propre à chaque conteneur. L’entrée extra_hosts doit donc être définie sur le conteneur de l’application, et non sur 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 lui-même ce nom. 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.
Configurez ensuite la route. Ajouter le 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 en attente, 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 bien 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 tout en restant inaccessible depuis l’interface publique. Vérifiez avec ss -lntp | grep 5432 sur l’hôte.
Ce que FIREWALL_OUTBOUND_SUBNETS modifie réellement
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 peuvent accéder. Elle précise également qu’elle entraîne des modifications du firewall et du routage. Les 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 subnets, car il bloque sinon le trafic sortant qui ne se dirige pas vers le serveur VPN.
Écrivez 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 que vous aviez en tête. Il concerne uniquement le trafic sortant : il régit les connexions qu’un conteneur initie. 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. Rien de tout cela ne coûte quoi que ce soit sur un tailnet personnel, même si les limites d’utilisateurs et d’appareils du forfait gratuit déterminent si cela reste vrai lorsque d’autres personnes doivent accéder aux mêmes interfaces. Comme la facturation compte les utilisateurs et non les machines, le coût réel d’un tailnet payant dépend du nombre de personnes que vous invitez, et non du nombre de conteneurs que vous leur exposez. Ces deux aspects demandent des interventions différentes.
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 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, le binding est 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 un hôte et un port, tailscale serve peut se placer devant ce port publié. Toutefois, serve et funnel ne permettent pas l’accès aux mêmes utilisateurs et un seul des deux conserve l’interface dans le tailnet. Cela évite également le problème décrit dans Docker qui publie directement les ports en contournant ufw.
Le trafic sortant est le point où FIREWALL_OUTBOUND_SUBNETS intervient. Si un conteneur doit appeler un pair, ajoutez l’adresse de ce pair et préférez un /32 par pair plutôt que l’ensemble de /10. Si la machine appelé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 plutôt que la propre adresse 100.x du routeur, puis vérifiez que l’hôte lui-même a accepté ces routes. Les noms MagicDNS ne seront 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 fixez-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 la structure courante
Un client de téléchargement derrière le VPN, 2 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
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 la structure, pas pour vous attarder sur les noms des produits. Les 2 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 qui résout les noms host.docker.internal. FIREWALL_OUTBOUND_SUBNETS définit 2 adresses uniques : l’adresse Docker bridge 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 comme un service système standard et écoute sur 172.17.0.1:5432. Il s’agit de la même architecture que une stack arr avec Docker Compose, avec la base de données déplacée hors de Docker.
Gardez la clé privée WireGuard hors du fichier Compose. ${WIREGUARD_PRIVATE_KEY} lit son contenu dans un fichier .env placé à côté, selon le modèle 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 conteneur ne démarre donc avant que le tunnel indique qu’il est opérationnel. Les healthchecks de Compose présentent la structure 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. Cela 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 le trafic
Exécutez deux fois la même requête : 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 les deux adresses correspondent, le trafic du conteneur ne passe pas par le tunnel. Tant que ce problème n’est pas corrigé, les autres correctifs 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. En dessous, vous devez voir une route pour chaque entrée de FIREWALL_OUTBOUND_SUBNETS, qui pointe vers la gateway du bridge Docker. Toute autre route qui sort par eth0 correspond à du trafic qui contourne le VPN.
Le control server de Gluetun renvoie la même adresse IP publique sur le port 8000, à l’adresse /v1/publicip/ip. Les versions récentes exigent de configurer l’authentification pour les routes du control server. Configurez-la avant de vous y fier.
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 en dehors du tunnel. Les deux vérifications d’adresse IP précédentes le détectent dès le premier lancement, car elles renvoient la même adresse.- Une plage plus large que la cible. Ouvrir
10.0.0.0/8pour atteindre une seule machine à10.0.1.7ouvre également toutes les adresses qu’un pair BitTorrent peut annoncer dans cette plage. Écrivez10.0.1.7/32. - Une plage qui chevauche les propres adresses du tunnel. La documentation de gluetun indique que cela force gluetun à envoyer 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. Indiquez les pairs nécessaires sous forme d’entrées/32.
N’oubliez pas que ce paramètre couvre 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 pour le 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 montre 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 alors que gluetun est arrêté échoue immédiatement :
Error response from daemon: cannot join network of a non running containerRedémarrer gluetun sur place provoque une panne plus discrète. Les conteneurs dépendants continuent de fonctionner alors que le namespace auquel ils étaient attachés est recréé sous-jacent. 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 d’image. 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: figure encore 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 le propre namespace réseau du 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 le namespace partagé.
Comment les autres conteneurs accèdent-ils à un service placé derrière gluetun ?
Les conteneurs situés dans le même namespace 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. La publication d’un port n’est pas 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 pouvoir établir une connexion, avec le préfixe le plus restrictif 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 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, qui correspond à la configuration DNS de gluetun lorsque le conteneur passe par gluetun. Vérifiez cela 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 sur ce conteneur.
Comment vérifier que le trafic passe toujours par le VPN ?
Exécutez une requête depuis le namespace, 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. Effectuez à nouveau cette vérification après chaque modification de FIREWALL_OUTBOUND_SUBNETS.