SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor

Docker et VPN : pourquoi les ports disparaissent

Avec Gluetun et network_mode: service, les ports et le nom DNS disparaissent. Découvrez l’erreur Docker et le fichier Compose corrigé pour les publier au bon endroit.

Pourquoi les ports disparaissent lorsque 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 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.

Laissez un bloc ports: sur le conteneur rattaché. Docker refuse alors de le créer :

Error response from daemon: conflicting options: port publishing and the container type network mode

L’outil utilisé ici est Gluetun. Il s’agit d’un conteneur qui se connecte à un fournisseur de VPN (virtual private network) commercial via WireGuard ou OpenVPN et qui intègre son propre pare-feu. La version v3.41.3 est la version actuelle en octobre 2026. Les exemples utilisent Mullvad avec WireGuard. Vous avez donc besoin d’un compte et d’une clé fournis par votre fournisseur. Si vous préférez terminer le tunnel sur du matériel qui vous appartient, exécuter votre propre serveur WireGuard sur un VPS configure l’autre extrémité. wg-easy dans Docker fournit une interface web pour cette configuration.

Ce que fait réellement network_mode: "service:gluetun"

Chaque conteneur Docker reçoit normalement son propre namespace réseau : ses propres interfaces, table de routage, règles de pare-feu et 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 l’adresse de gluetun.
  • L’application n’est connectée à aucun réseau Docker. Son nom de service n’est donc jamais enregistré et ne peut jamais être résolu. Les autres conteneurs doivent utiliser gluetun.
  • Les conteneurs présents dans le namespace communiquent entre eux via localhost.
  • Deux conteneurs dans un même namespace ne peuvent pas écouter sur le même port. La documentation de Gluetun est catégorique : il n’existe aucune solution de contournement.
  • Les capabilities appartiennent à un conteneur, pas à un namespace. Gluetun détient NET_ADMIN et /dev/net/tun, car il crée l’interface du tunnel. Le conteneur attaché ne les hérite pas.
  • Compose rejette tout fichier dans lequel un service définit à la fois network_mode et networks. Connectez gluetun à vos réseaux, et l’application les utilisera indirectement.

Redémarrer gluetun déconnecte tout ce qui lui est attaché. Ce comportement est documenté. C’est pourquoi gluetun redémarre le processus VPN à l’intérieur du conteneur au lieu de quitter lorsque la connexion échoue. Après avoir redémarré ou recréé gluetun vous-même, redémarrez les conteneurs qui lui sont attachés.

Le fichier Compose fonctionnel

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-stopped

Le 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 transmet le trafic de l’hôte au port 8080 dans ce namespace. Si vous modifiez un nombre sans modifier l’autre, le port ne répond pas. 127.0.0.1:8080:8080 maintient 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 pare-feu. 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 -30

docker 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 détermine 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 propre serveur, l’application n’est pas dans le tunnel. Rien de ce qui suit ne fonctionnera alors comme décrit.

Conservez les clés en dehors du fichier Compose

gluetun.env contient les identifiants et ne doit pas être versionné dans git :

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

Les deux valeurs proviennent d’un fichier de configuration WireGuard que vous générez dans l’espace client de votre fournisseur. Définissez ses permissions sur 600. Soyez clair sur ce que cela apporte : la clé reste en dehors 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 à l’intérieur

Les deux directions fonctionnent, et chacune utilise un nom différent. Les deux conteneurs doivent partager un réseau Docker. Il s’agit du réseau de gluetun, car le conteneur attaché n’a pas de réseau propre. Fonctionnement des réseaux Docker Compose décrit 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 é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 ne passe jamais par 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. Gluetun résout les noms des autres conteneurs depuis son namespace depuis la version v3.41. Utilisez donc cette version ou une version ultérieure si un nom ne se résout pas.

Le firewall de Gluetun détermine qui peut ouvrir une connexion vers lui. Le trafic provenant de son propre réseau Docker 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 a besoin d’une route de retour autorisée par le firewall et le routage de Gluetun :

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

La signification documentée est précise : il s’agit de 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 constituent 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 sert donc à rien dans ce cas. Vous avez besoin d’un port redirigé par votre fournisseur, puis de l’indiquer dans FIREWALL_VPN_INPUT_PORTS, qui autorise les ports du côté du serveur VPN. Mullvad, utilisé dans cet exemple, ne prend pas en charge la redirection de ports. Vous devez donc utiliser un fournisseur qui la prend en charge. C’est l’élément que la plupart des stacks media construits avec Docker Compose laissent mal configuré.

Le kill switch : que se passe-t-il lorsque le tunnel tombe

Ce schéma justifie sa complexité en cas de défaillance. Le conteneur attaché partage les interfaces et la table de routage de Gluetun. Le firewall de Gluetun bloque le trafic Internet normal en dehors du tunnel, tout en autorisant le point de terminaison du serveur VPN, le trafic Docker local et les exceptions configurées. C’est ce firewall qui empêche le basculement vers l’interface classique lorsque le tunnel tombe. Aucun paquet ne peut 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 indiquées dans HEALTH_ICMP_TARGET_IPS, qui sont par défaut 1.1.1.1,8.8.8.8. Si ICMP n’est pas autorisé, ce contrôle utilise à la place une requête DNS UDP en clair. Toutes les cinq minutes, il établit une connexion TCP et TLS (Transport Layer Security) complète vers HEALTH_TARGET_ADDRESSES, dont la valeur par défaut est cloudflare.com:443,github.com:443. Si ces contrôles é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 timeout

Consultez les journaux du conteneur attaché en tenant compte de cet ordre. Les lignes telles que connection refused, operation not permitted et i/o timeout dans l’application sont les conséquences d’un tunnel hors service, pas les causes. La documentation de Gluetun le précise explicitement, car on signale souvent la conséquence et on cherche sa cause pendant des heures.

HEALTH_RESTART_VPN=on est la valeur par défaut et doit rester activée. Désactivez-la uniquement pour diagnostiquer une défaillance précise, car un tunnel hors service le reste lorsqu’elle est désactivée.

Ordre de démarrage : empêcher le stack de démarrer avant que le tunnel soit opérationnel

L’image fournit un healthcheck Docker :

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck

Cette commande lance une seconde instance éphémère de gluetun, qui interroge le health server de l’instance active sur http://127.0.0.1:9999/. Un tunnel fonctionnel répond 200 OK. Un tunnel défaillant répond 500 Internal server error avec une chaîne d’erreur, et Docker marque le conteneur comme unhealthy après trois vérifications consécutives en échec.

condition: service_healthy attend ce résultat. 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 avec un réseau inactif et abandonne souvent sa première tentative de connexion. Healthchecks dans Docker Compose détaille la syntaxe et les paramètres de temporisation.

Une limite peut poser problème. Compose évalue cette condition au démarrage du stack. Il n’arrête pas et ne redémarre pas l’application si gluetun devient ensuite unhealthy. L’auto-healing interne de gluetun couvre ce cas. C’est pourquoi gluetun redémarre le processus VPN plutôt que le conteneur.

Vérifier l'absence de fuite DNS avant de valider la configuration

Le DNS (Domain Name System) est la fuite qui persiste malgré un tunnel correctement établi. Gluetun exécute son propre résolveur dans son espace de noms 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 paramètres pour garantir que vos recherches sont chiffrées et transitent par le tunnel.

Le paramètre qui compromet cette sécurité lorsqu'il est dirigé vers un résolveur local est DNS_UPSTREAM_PLAIN_ADDRESSES. Les utilisateurs y ont recours lorsqu'un nom ne se résout pas et qu'ils souhaitent que leur routeur ou le résolveur de leur fournisseur d'accès réponde à la place. La documentation de Gluetun avertit que les requêtes envoyées à un résolveur sur votre pont Docker ou votre réseau local fuient en dehors du tunnel VPN. Un résolveur public en clair reçoit toujours les requêtes via le tunnel, mais le fournisseur VPN peut les lire et les modifier. La version WireGuard de cette même erreur est traitée dans DNS qui cesse de se résoudre sur un tunnel WireGuard.

Pour effectuer un test, définissez SHADOWSOCKS=on et SHADOWSOCKS_PASSWORD sur gluetun, publiez 8388:8388/tcp et 8388:8388/udp, puis connectez-vous avec un client Shadowsocks configuré pour tunneler le DNS en UDP et chargez un test de fuite DNS. Le résultat doit indiquer votre fournisseur ou Cloudflare, jamais votre routeur domestique. La documentation de Gluetun prévient que certains tests de fuite rapportent des résultats étranges, car le résolveur à l'intérieur de l'espace de noms est un intermédiaire de cache local plutôt que le serveur qui répond finalement. Considérez un pays erroné ou le résolveur de votre propre FAI comme un signal d'alerte réel.

Ajouter Tailscale à côté du sidecar VPN, et lequel prend le dessus

Tailscale est un réseau overlay basé sur WireGuard pour accéder à vos propres machines. On l'exécute souvent à côté d'un VPN fournisseur pour conserver un accès d'administration vers la pile. Partager un namespace implique aussi de partager les règles de pare-feu ; les deux doivent donc avoir des configurations de routage et de pare-feu compatibles. La documentation de Tailscale définit le comportement par défaut : il agit comme un réseau overlay, il achemine uniquement le trafic entre les appareils exécutant Tailscale et ne touche pas à votre trafic internet public.

La réponse dépend donc d'un paramètre.

  • Tailscale dans son propre conteneur, configuration par défaut : il ne voit jamais le trafic sortant de l'application. Gluetun achemine tout le trafic. Un processus dans le conteneur Tailscale atteint l'application sur gluetun:8080 via un réseau Docker partagé ; exposer cette connexion aux clients du tailnet nécessite un transfert ou un proxy.
  • Tailscale attaché au namespace de gluetun avec network_mode: "service:gluetun" : en mode réseau userspace par défaut, TS_USERSPACE est activé, tailscaled ne crée aucune interface et fonctionne comme un proxy SOCKS5 ou HTTP, il ne peut donc pas modifier le routage. Gluetun achemine toujours le trafic sortant ordinaire, sauf si une application utilise explicitement ce proxy.
  • Le même cas, avec TS_USERSPACE=false : Tailscale a besoin de son propre périphérique /dev/net/tun et de capacités spécifiques, car ils ne sont pas fournis avec le namespace. Tailscaled crée un périphérique tunnel et, sous Linux, installe normalement des routes individuelles IPv4 peer /32 plutôt qu'une route unique pour 100.64.0.0/10. Définir TS_USERSPACE=false seul n'active pas la route agrégée. Il installe les routes de sous-réseau des autres nœuds uniquement si vous passez --accept-routes via TS_EXTRA_ARGS. TS_ROUTES fonctionne dans l'autre sens : il annonce les sous-réseaux que ce conteneur peut atteindre au reste de votre tailnet, l'équivalent de tailscale set --advertise-routes=, et n'ajoute aucune route ici. Le trafic public sort toujours via gluetun.
  • Réseau noyau dans le namespace de gluetun avec un exit node sélectionné, sudo tailscale set --exit-node=<exit-node-ip> : Tailscale installe des routes par défaut dans ce namespace, mais le pare-feu de Gluetun peut bloquer ce chemin. Sélectionner un exit node dans un conteneur séparé ne modifie pas les routes de l'application, et le mode userspace achemine uniquement le trafic explicitement envoyé via son proxy. Ne combinez pas des routes par défaut concurrentes dans le namespace de gluetun. Une seule route par défaut, un seul propriétaire.

Si ces routes annoncées sont l'objectif, et que vous souhaitez que l'ensemble du réseau privé derrière la machine soit accessible plutôt que la machine seule, exécuter un routeur de sous-réseau Tailscale sur un VPS couvre l'approbation de route, le transfert IP et le flag côté client que TS_ROUTES seul ne traite pas.

Si Tailscale est présent pour vous fournir une URL d'administration plutôt qu'une route, tailscale serve et tailscale funnel placent HTTPS devant un service HTTP local, avec seulement le funnel l'ouvrant à l'internet public. Dans Tailscale v1.104.5, leurs reverse proxies HTTP acceptent http://gluetun:8080 directement lorsque le conteneur Tailscale peut l'atteindre via un réseau Docker partagé. Incluez le schéma http:// pour une cible autre que localhost. Partager le network namespace de gluetun et cibler http://127.0.0.1:8080 fonctionne également.

Un effet secondaire est visible lorsque Tailscale s'exécute à l'intérieur du tunnel. Ses pairs voient l'adresse du fournisseur VPN, attendez-vous donc à ce qu'il bascule plus souvent vers des relais. tailscale status affiche relay "..." à côté d'un pair au lieu de direct lorsque cela se produit. La connexion fonctionne mais elle est plus lente. Si l'overlay est la seule chose dont vous aviez réellement besoin, la différence entre WireGuard simple et Tailscale est un meilleur point de départ.

Ce qui échoue et les messages associés

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 présent sur le service attaché. Déplacez-le vers gluetun.

Compose refuse tout le fichier. Un service ne peut pas définir à la fois network_mode et networks. Placez les réseaux sur gluetun.

Un autre conteneur ne parvient pas à résoudre l'application. curl: (6) Could not resolve host: qbittorrent est un comportement normal, car le conteneur attaché n'a rejoint aucun réseau et n'a enregistré aucun nom. Utilisez gluetun et le port.

Le second conteneur attaché ne démarre pas. Deux processus dans un même espace de nom ne peuvent pas écouter sur le même port, et celui qui échoue signale que l'adresse est déjà utilisée. Modifiez le port interne de l'application ou lancez un second gluetun.

L'application n'a plus de réseau après une modification de gluetun. Redémarrer ou recréer gluetun coupe la connectivité de tous les éléments qui y 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 équipement sur le chemin rejette les paquets trop volumineux sans renvoyer d'erreur. Vérifiez WIREGUARD_MTU, dont la valeur par défaut dans la version 3.41.3 est 1320, et réduisez-la si le chemin nécessite des paquets plus petits. L'image de développement utilise la découverte du MTU du chemin (path MTU discovery) lorsque ce paramètre n'est pas défini.

Gluetun ne passe jamais à l'état healthy. La vérification au démarrage indique les premiers suspects : 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 votre pare-feu 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, et qu’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 se trouver 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 la créera même pas : Error response from daemon: conflicting options: port publishing and the container type network mode.

Comment accéder à un conteneur situé dans le tunnel VPN depuis un conteneur qui en est à l’extérieur ?

Utilisez le nom de service de gluetun et le port sur lequel l’application écoute, par exemple gluetun:8080. Le conteneur attaché n’est connecté à aucun réseau Docker qui lui soit propre ; son propre nom ne peut donc jamais être résolu. La publication d’un port n’est pas 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 portable sur votre LAN, a besoin d’un chemin de retour autorisé par le pare-feu et le routage de gluetun ; ajoutez ce sous-réseau à FIREWALL_OUTBOUND_SUBNETS.

Gluetun fonctionne-t-il comme un kill switch lorsque le VPN tombe ?

Oui, si son pare-feu est activé. Le conteneur attaché partage les interfaces et les routes de Gluetun. Le pare-feu bloque le trafic Internet ordinaire en dehors du tunnel, tout en autorisant le point de terminaison du serveur VPN, le trafic Docker local et les exceptions configurées. 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, car 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 achemine le trafic sortant ordinaire, sauf si vous le faites explicitement passer par Tailscale. 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 de conteneur, il ne crée aucune interface ; il ne peut donc pas modifier le routage. Avec TS_USERSPACE=false, Linux obtient normalement des routes IPv4 individuelles en /32 vers les pairs, plutôt qu’une route agrégée 100.64.0.0/10, ainsi que les routes vers les sous-réseaux des autres nœuds uniquement si vous ajoutez --accept-routes via TS_EXTRA_ARGS. TS_ROUTES publie les sous-réseaux de ce conteneur dans votre tailnet et n’ajoute aucune route locale. La sélection d’un exit node avec sudo tailscale set --exit-node=<exit-node-ip> installe les routes par défaut uniquement dans l’espace de noms qui utilise le networking kernel de Tailscale ; un conteneur distinct ne modifie pas les routes de l’application, et le mode userspace ne transporte que le trafic envoyé par son proxy. Choisissez un seul produit pour gérer la route par défaut, au lieu de superposer les deux.

Cet article vous a-t-il été utile ?