Pourquoi les ports Docker disparaissent derrière un VPN
Avec Gluetun et network_mode: service, les ports et le nom de service disparaissent. Découvrez l’erreur exacte et le fichier Compose qui fonctionne.
Pourquoi les ports disparaissent quand vous faites passer des conteneurs Docker par un VPN
Pour faire passer des conteneurs Docker par un VPN, vous donnez le tunnel à un conteneur, puis vous rattachez les autres à son network namespace avec network_mode: "service:gluetun". C’est ce rattachement qui surprend souvent. Le conteneur rattaché n’a alors plus son propre réseau. Ses ports publiés et son nom de service Docker disparaissent donc avec lui. Publiez les ports sur le conteneur VPN. Les autres conteneurs accèdent alors à l’application avec le nom du conteneur VPN.
Si vous laissez un bloc ports: sur le conteneur rattaché, Docker refuse même de le créer :
Error response from daemon: conflicting options: port publishing and the container type network modeL’outil utilisé ici est Gluetun. Il s’agit d’un conteneur qui se connecte à un fournisseur de VPN (virtual private network) commercial avec WireGuard ou OpenVPN et qui intègre son propre firewall. La version v3.41.3 est la version actuelle en août 2026. Les exemples utilisent Mullvad avec WireGuard. Vous avez donc besoin d’un compte et d’une key fournie par votre fournisseur. Si vous préférez terminer le tunnel sur du matériel que vous possédez, configurer votre propre serveur WireGuard sur un VPS crée l’autre extrémité, et wg-easy dans Docker fournit une interface web pour le gérer.
Ce que network_mode: "service:gluetun" fait réellement
Chaque conteneur Docker possède normalement son propre network namespace : ses propres interfaces, sa table de routage, ses règles de pare-feu et ses sockets en écoute. Le mode service: ignore cette étape et démarre le conteneur dans le namespace de gluetun. Un seul namespace signifie une seule adresse IP, ce qui entraîne six conséquences.
- L’application n’a pas sa propre adresse. Elle utilise celle de gluetun.
- L’application n’est connectée à aucun réseau Docker. Son nom de service n’est donc jamais enregistré ni résolu. Les autres conteneurs doivent utiliser
gluetun. - Les conteneurs présents dans le namespace communiquent via
localhost. - Deux conteneurs présents dans le même namespace ne peuvent pas écouter sur le même port. La documentation de Gluetun est claire : il n’existe pas de solution de contournement.
- Les capabilities appartiennent à un conteneur, pas à un namespace. Gluetun possède
NET_ADMINet/dev/net/tun, car il crée l’interface du tunnel. Le conteneur attaché n’en hérite pas. - Compose refuse tout fichier dans lequel un service définit à la fois
network_modeetnetworks. Connectez gluetun à vos réseaux, et l’application les utilise également.
Redémarrer gluetun déconnecte tout ce qui y est attaché. Ce comportement est documenté. C’est pourquoi gluetun redémarre le processus VPN dans le conteneur au lieu de quitter lorsque la connexion échoue. Après avoir vous-même redémarré ou recréé gluetun, redémarrez les conteneurs qui y sont attachés.
Le fichier compose qui fonctionne
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedLe tag :v3 correspond à la dernière version stable de la série v3. Le tag :latest pointe vers le dernier commit de la branche master, qui correspond à la version de développement. Épinglez donc :v3 sur une machine que vous ne voulez pas devoir déboguer un mardi.
WEBUI_PORT=8080 doit correspondre au port publié, car qBittorrent s’attache dans le namespace de gluetun et la règle de publication redirige le trafic de l’hôte vers le port 8080 dans ce namespace. Si vous modifiez un seul des deux nombres, le port ne répond plus. 127.0.0.1:8080:8080 conserve l’interface web sur l’adresse loopback de l’hôte. Un 8080:8080 seul publie le port sur toutes les interfaces et ajoute sa propre règle de firewall. C’est ainsi que les ports publiés par Docker contournent directement ufw.
Démarrez les conteneurs, puis vérifiez-les dans cet ordre :
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps doit afficher gluetun avec l’état healthy et qbittorrent avec l’état running. Vérifiez ensuite l’adresse de sortie depuis le namespace. C’est le contrôle qui permet de valider tout le reste :
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"Le champ ip de ce JSON doit contenir l’adresse de votre fournisseur VPN. S’il contient l’adresse de votre serveur, l’application n’est pas dans le tunnel et rien de ce qui suit ne fonctionnera comme décrit.
Garder les clés hors du fichier Compose
gluetun.env contient les identifiants et reste hors de git :
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Les deux valeurs proviennent d’un fichier de configuration WireGuard que vous générez dans l’espace client de votre fournisseur. Définissez les permissions du fichier sur 600. Soyez précis sur ce que cela apporte : la clé reste hors de votre dépôt, mais docker inspect gluetun affiche toujours toutes les variables d’environnement à toute personne pouvant accéder au socket Docker. Fichiers d’environnement et secrets dans Docker Compose présente des options plus robustes.
Comment un conteneur extérieur au tunnel communique avec un conteneur qui s’y trouve
Les deux sens fonctionnent, et chacun utilise un nom différent. Les deux conteneurs doivent partager un réseau Docker, c’est-à-dire le réseau de gluetun, puisque le conteneur qui lui est rattaché n’a pas de réseau propre. La section Fonctionnement des réseaux Docker dans Docker Compose présente les valeurs par défaut.
Pour communiquer de l’extérieur vers l’intérieur, utilisez le nom de gluetun et le port sur lequel l’application est en écoute. Un conteneur de reverse proxy atteint l’interface web de qBittorrent à l’adresse gluetun:8080. Aucune entrée ports: n’est nécessaire, car le trafic entre conteneurs reste sur le réseau Docker et n’utilise jamais un port de l’hôte.
Pour communiquer de l’intérieur vers l’extérieur, utilisez le nom de service de l’autre conteneur, par exemple postgres:5432. Depuis la version v3.41, Gluetun résout les noms des autres conteneurs dans son propre espace de noms. Utilisez donc cette version ou une version ultérieure si un nom n’est pas résolu.
Le pare-feu de Gluetun détermine qui peut ouvrir une connexion vers lui. Le trafic provenant du propre réseau Docker de gluetun est autorisé. Un client situé sur un autre sous-réseau, un ordinateur portable sur votre LAN ou un conteneur sur un réseau bridge distinct est bloqué tant que vous n’avez pas déclaré ce sous-réseau :
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24La signification documentée est précise : il s’agit des sous-réseaux séparés par des virgules auxquels Gluetun et les conteneurs qui partagent sa pile réseau sont autorisés à accéder.
Les connexions entrantes depuis Internet posent un problème distinct. Les pairs d’un client torrent arrivent du côté du VPN. Publier le port 6881 sur l’hôte ne leur sert donc à rien. Vous avez besoin d’un port transféré par votre fournisseur, puis de déclarer ce port dans FIREWALL_VPN_INPUT_PORTS, qui autorise les ports du côté du serveur VPN. C’est l’élément qui reste le plus souvent incorrect dans les stacks multimédias construits avec Docker Compose.
Le coupe-circuit : que se passe-t-il lorsque le tunnel tombe ?
Ce schéma justifie sa complexité en cas de panne. Le conteneur attaché n’a pas de seconde route. Sa seule sortie de la machine passe par le namespace qu’il partage. Lorsque le tunnel est interrompu, il n’a donc aucun chemin de secours. Le firewall de Gluetun impose la même règle de l’autre côté : le trafic sortant passe par le tunnel ou vers l’endpoint du serveur VPN, et tout le reste est bloqué. Aucun intervalle ne permet aux paquets de sortir par l’interface classique pendant qu’un client se reconnecte.
Gluetun surveille sa propre connexion. Toutes les minutes, il envoie un écho ICMP (un ping) aux adresses de HEALTH_ICMP_TARGET_IPS, qui correspondent par défaut à 1.1.1.1,8.8.8.8. Toutes les cinq minutes, il établit une connexion TCP et TLS (Transport Layer Security) complète vers HEALTH_TARGET_ADDRESSES, avec cloudflare.com:443,github.com:443 comme valeur par défaut. Lorsque ces tests échouent, il redémarre le VPN dans le conteneur et l’inscrit dans les journaux :
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutLisez les journaux du conteneur attaché en tenant compte de cet ordre des événements. Des lignes comme connection refused, operation not permitted et i/o timeout dans l’application sont les conséquences d’un tunnel interrompu, et non les causes. La documentation de Gluetun le précise explicitement, car on signale souvent la conséquence avant de la rechercher pendant des heures.
HEALTH_RESTART_VPN=on est activé par défaut et doit le rester. Désactivez-le uniquement pour diagnostiquer une panne précise, car un tunnel interrompu reste interrompu lorsqu’il est désactivé.
Ordre de démarrage : empêcher la stack de démarrer avant l’établissement du tunnel
L’image fournit un healthcheck Docker :
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckCette commande lance une deuxième copie de courte durée de gluetun. Elle interroge le health server de l’instance en cours d’exécution sur http://127.0.0.1:9999/. Un tunnel fonctionnel renvoie 200 OK. Un tunnel défaillant renvoie 500 Internal server error avec une chaîne d’erreur, et le conteneur est marqué comme non sain après un seul échec.
condition: service_healthy attend cet état. Le simple depends_on: [gluetun] attend uniquement que le conteneur démarre. Cela se produit plusieurs secondes avant la fin du handshake. L’application démarre donc sur un réseau indisponible et abandonne souvent dès sa première tentative de connexion. Les healthchecks dans Docker Compose présente la syntaxe et les paramètres de temporisation.
Une limite est souvent source d’erreurs. Compose évalue cette condition une seule fois, lors de la création du conteneur. Il n’arrête ni ne redémarre ensuite l’application si gluetun devient non sain. L’auto-healing interne de gluetun couvre ce cas. C’est pourquoi il redémarre le processus VPN plutôt que le conteneur.
Vérifiez l’absence de fuite DNS avant de faire confiance à la configuration
Le DNS (domain name system) est la fuite qui subsiste même lorsque le tunnel est correctement configuré. Gluetun exécute son propre résolveur dans le namespace et transfère les requêtes via DoT (DNS over TLS) vers Cloudflare par défaut : DNS_UPSTREAM_RESOLVER_TYPE=dot et DNS_UPSTREAM_RESOLVERS=cloudflare. Ne modifiez pas ces deux paramètres. Vos résolutions DNS sont ainsi chiffrées et passent par le tunnel.
Le paramètre qui provoque cette fuite est DNS_UPSTREAM_PLAIN_ADDRESSES. On l’utilise lorsque la résolution d’un nom échoue et que l’on veut faire répondre le routeur ou le résolveur du fournisseur d’accès. La documentation de Gluetun précise clairement la conséquence : tout le trafic DNS ne passe pas par le tunnel VPN et fuit à l’extérieur. Votre trafic reste privé. Votre liste de noms d’hôte ne l’est pas. La même erreur avec WireGuard est traitée dans DNS qui cesse de fonctionner dans un tunnel WireGuard.
Pour effectuer le test, définissez HTTPPROXY=on sur gluetun et publiez 8888:8888/tcp, puis configurez un navigateur pour utiliser ce proxy et ouvrez un test de fuite DNS. Le résultat doit indiquer votre fournisseur d’accès ou Cloudflare, jamais votre routeur domestique. La documentation de Gluetun précise également que certains tests de fuite peuvent afficher des résultats inhabituels, car le résolveur présent dans le namespace est un intermédiaire local avec mise en cache, et non le serveur qui fournit finalement la réponse. Considérez comme un signal réel l’affichage d’un mauvais pays ou du résolveur de votre propre FAI.
Ajouter Tailscale à côté du sidecar VPN, et déterminer lequel prend le dessus
Tailscale est un réseau overlay basé sur WireGuard qui permet d’accéder à vos propres machines. Certains l’exécutent à côté d’un VPN provider pour conserver un accès d’administration à la stack. Les deux interfèrent rarement, pour une raison qu’il est utile de comprendre. La documentation de Tailscale indique le comportement par défaut : Tailscale agit comme un réseau overlay, route uniquement le trafic entre les appareils qui exécutent Tailscale et ne modifie pas le trafic Internet public.
La réponse dépend donc d’un seul paramètre.
- Tailscale dans son propre conteneur, avec la configuration par défaut : il ne voit jamais le trafic sortant de l’application. Gluetun le prend entièrement en charge. Tailscale accède à l’application via
gluetun:8080, comme n’importe quel autre conteneur externe. - Tailscale attaché au namespace de gluetun avec
network_mode: "service:gluetun": il a besoin de son proprecap_adddenet_adminetnet_raw, car les capabilities ne sont pas incluses avec le namespace. En mode de network userspace par défaut,TS_USERSPACEest activé, tailscaled ne crée aucune interface et fonctionne comme un proxy SOCKS5 ou HTTP. Il ne peut donc pas modifier le routage. Gluetun continue de prendre en charge tout le trafic. - Même configuration, avec
TS_USERSPACE=false: tailscaled crée un tunnel device et installe des routes, mais uniquement pour la plage tailnet100.64.0.0/10et pour les routes de sous-réseau que vous publiez avecTS_ROUTES. Le trafic public continue de sortir par gluetun. - L’une des configurations précédentes avec un exit node sélectionné,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale s’approprie la route par défaut et prend le dessus. Ne combinez pas cette configuration avec gluetun. Une seule route par défaut peut avoir un seul propriétaire.
Un effet secondaire est visible lorsque Tailscale s’exécute à l’intérieur du tunnel. Ses pairs voient l’adresse du VPN provider. Tailscale utilise donc plus souvent des relays. tailscale status affiche relay "..." à côté d’un pair au lieu de direct dans ce cas. La connexion fonctionne, mais elle est plus lente. Si le réseau overlay est le seul élément dont vous avez réellement besoin, la différence entre WireGuard classique et Tailscale constitue un meilleur point de départ.
Ce qui échoue et le message affiché
Docker refuse de créer le conteneur de l’application. Error response from daemon: conflicting options: port publishing and the container type network mode signifie qu’un bloc ports: est toujours défini sur le service attaché. Déplacez-le vers gluetun.
Compose refuse l’ensemble du fichier. Un service ne peut pas définir à la fois network_mode et networks. Définissez les réseaux sur gluetun.
Un autre conteneur ne peut pas résoudre l’application. curl: (6) Could not resolve host: qbittorrent est le comportement attendu, car le conteneur attaché n’a rejoint aucun réseau et n’a enregistré aucun nom. Utilisez gluetun et le port.
Le deuxième conteneur attaché ne démarre pas. Deux processus dans un même namespace ne peuvent pas utiliser le même port. Celui qui échoue signale que l’adresse est déjà utilisée. Modifiez le port interne de l’application ou exécutez un deuxième gluetun.
L’application n’a plus accès au réseau après une modification de gluetun. Le redémarrage ou la recréation de gluetun interrompt la connectivité de tous les conteneurs qui lui sont attachés. Redémarrez ces conteneurs.
Les petites pages se chargent, mais les grandes restent bloquées. Il s’agit du MTU (maximum transmission unit). Le tunnel ajoute une surcharge, et un élément du chemin abandonne les paquets trop volumineux sans renvoyer d’erreur. Diminuez WIREGUARD_MTU, essayez 1400, puis 1320.
Gluetun ne devient jamais healthy. Le contrôle de démarrage indique les premiers éléments à vérifier : WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Vérifiez si la clé a expiré, puis si la liste des serveurs est obsolète et, enfin, si le firewall de l’hôte bloque le trafic UDP sortant.
FAQ
Pourquoi les ports publiés de mon conteneur ne fonctionnent-ils plus derrière Gluetun ?
Parce que network_mode: "service:gluetun" place le conteneur dans l’espace de noms réseau de gluetun. Un espace de noms possède une seule adresse IP et un seul ensemble de ports en écoute. L’application continue d’écouter, mais la règle de publication doit être définie sur le conteneur qui possède cet espace de noms. Déplacez la liste ports: vers le service gluetun. Si vous l’avez laissée sur le service attaché, Docker ne le créera même pas : Error response from daemon: conflicting options: port publishing and the container type network mode.
Comment joindre, depuis un conteneur extérieur au tunnel VPN, un conteneur qui s’y trouve ?
Utilisez le nom de service de gluetun et le port sur lequel l’application écoute, par exemple gluetun:8080. Le conteneur attaché ne possède aucun réseau Docker propre. Son propre nom ne peut donc jamais être résolu. Aucune publication de port n’est nécessaire pour le trafic entre conteneurs. Dans l’autre sens, un conteneur situé dans l’espace de noms peut joindre un conteneur extérieur avec son nom de service, par exemple postgres:5432, avec Gluetun v3.41 et les versions ultérieures. Un client situé sur un autre sous-réseau, par exemple un ordinateur portable sur votre réseau local, est bloqué par le firewall de gluetun jusqu’à ce que vous ajoutiez ce sous-réseau à FIREWALL_OUTBOUND_SUBNETS.
Gluetun fonctionne-t-il comme un kill switch lorsque le VPN tombe ?
Oui, pour deux raisons. Le conteneur attaché n’a aucune route autre que celle de l’espace de noms partagé. Si le tunnel tombe, il n’a donc plus de chemin vers l’extérieur de la machine. Le firewall de Gluetun autorise également le trafic sortant uniquement via le tunnel et vers le endpoint du serveur VPN. Gluetun redémarre ensuite le VPN en interne et journalise WARN [vpn] restarting VPN because it failed to pass the healthcheck, au lieu de quitter. En effet, chaque conteneur attaché perd son réseau lorsque gluetun redémarre lui-même.
Tailscale et Gluetun dans la même stack : lequel achemine le trafic sortant ?
Gluetun, dans toutes les configurations sauf une. Par défaut, Tailscale achemine uniquement le trafic entre les appareils de votre tailnet et laisse le trafic public inchangé. Dans le mode userspace par défaut de l’image du conteneur, il ne crée aucune interface. Il ne peut donc pas modifier le routage. Avec TS_USERSPACE=false, il installe uniquement les routes vers 100.64.0.0/10 et vers les sous-réseaux annoncés. L’exception est un exit node : sudo tailscale set --exit-node=<exit-node-ip> fait de Tailscale la route par défaut, qui devient alors prioritaire. Choisissez un seul produit pour gérer la route par défaut au lieu de superposer les deux.