Docker container via VPN routeren met Gluetun
Wanneer u een container koppelt aan de netwerk-namespace van Gluetun, vervallen gepubliceerde poorten. Leer hoe u dit oplost met de juiste Docker Compose configuratie.
Waarom poorten verdwijnen wanneer u Docker-containers via een VPN routeert
Om Docker-containers via een VPN te routeren, geeft u één container de tunnel en koppelt u de overige containers aan de netwerk-namespace daarvan met network_mode: "service:gluetun". Die koppeling is het onderdeel dat gebruikers vaak verrast. De gekoppelde container heeft geen eigen netwerk meer, waardoor de gepubliceerde poorten en de Docker-servicenaam komen te vervallen. Publiceer de poorten in plaats daarvan op de VPN-container; andere containers bereiken de applicatie vervolgens via de naam van de VPN-container.
Laat u een ports:-blok staan op de gekoppelde container, dan weigert Docker deze volledig aan te maken:
Error response from daemon: conflicting options: port publishing and the container type network modeDe tool die hiervoor wordt gebruikt is Gluetun, een container die verbinding maakt met een commerciële VPN-provider (virtual private network) via WireGuard of OpenVPN en over een eigen firewall beschikt. Release v3.41.3 is de huidige versie per augustus 2026. De voorbeelden maken gebruik van Mullvad met WireGuard; u heeft hiervoor een account en een sleutel van uw provider nodig. Als u de tunnel liever op eigen hardware beëindigt, bouwt uw eigen WireGuard-server op een VPS draaien de andere kant van de verbinding, en wg-easy in Docker verpakt dit in een webinterface.
Wat network_mode: "service:gluetun" daadwerkelijk doet
Elke Docker-container krijgt normaal gesproken een eigen netwerk-namespace: eigen interfaces, een eigen routeringstabel, firewallregels en listening sockets. De service:-modus slaat die stap over en start de container binnen de namespace van gluetun. Eén namespace betekent één IP-adres, en dat verandert zes zaken.
- De applicatie heeft geen eigen adres. Het adres is dat van gluetun.
- De applicatie is aan geen enkel Docker-netwerk gekoppeld, waardoor de servicenaam nooit wordt geregistreerd en nooit wordt omgezet. Andere containers moeten
gluetungebruiken. - Containers binnen de namespace bereiken elkaar via
localhost. - Twee containers in één namespace kunnen niet op dezelfde poort luisteren. De documentatie van gluetun is hier duidelijk over: er is geen workaround.
- Capabilities horen bij een container, niet bij een namespace. Gluetun bezit
NET_ADMINen/dev/net/tunomdat het de tunnel-interface aanmaakt. De gekoppelde container erft deze niet. - Compose weigert elk bestand waarin één service zowel
network_modealsnetworksinstelt. Koppel gluetun aan uw netwerken, dan lift de applicatie mee.
Het herstarten van gluetun verbreekt de verbinding met alles wat eraan gekoppeld is. Dat is gedocumenteerd gedrag, en het is de reden waarom gluetun het VPN-proces binnen de container herstart in plaats van af te sluiten wanneer de verbinding wegvalt. Nadat u gluetun zelf herstart of opnieuw aanmaakt, moet u de containers die eraan gekoppeld zijn ook herstarten.
Het werkende compose-bestand
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-stoppedDe tag :v3 is de nieuwste stabiele release in de v3-serie. De tag :latest verwijst naar de laatste commit van de master-branch; dit is de ontwikkelversie. Gebruik daarom :v3 niet op een machine waar u op dinsdag geen fouten wilt opsporen.
WEBUI_PORT=8080 moet overeenkomen met de gepubliceerde poort, omdat qBittorrent bindt binnen de namespace van gluetun en de publish-regel hostverkeer naar poort 8080 daarheen stuurt. Als u het ene getal wijzigt zonder het andere, reageert de poort nergens op. 127.0.0.1:8080:8080 houdt de webinterface op het loopback-adres van de host. Een kale 8080:8080 publiceert op elke interface en schrijft zijn eigen firewall-regel; dit is de reden waarom Docker gepubliceerde poorten direct langs ufw glippen.
Start de container en controleer deze vervolgens in deze volgorde:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps zou gluetun als healthy en qbittorrent als running moeten tonen. Bevestig daarna het exit-adres vanuit de namespace; dit is de controle die allesbepalend is:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"Het veld ip in die JSON zou het adres van uw VPN-provider moeten zijn. Als dit het eigen adres van uw server is, bevindt de applicatie zich niet in de tunnel en zal niets hieronder zich gedragen zoals beschreven.
Houd de keys buiten het compose-bestand
gluetun.env bevat de inloggegevens en deze blijven buiten git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32Beide waarden zijn afkomstig uit een WireGuard-configuratiebestand dat u genereert in de accountomgeving van uw provider. Zet het bestand op modus 600. Wees realistisch over wat dit oplevert: de key blijft buiten uw repository, maar docker inspect gluetun print nog steeds elke omgevingsvariabele naar iedereen die toegang heeft tot de Docker-socket. Omgevingsbestanden en secrets in Docker Compose behandelt de robuustere opties.
Hoe een container buiten de tunnel communiceert met een container daarbinnen
Beide richtingen werken en gebruiken elk een andere naam. De twee containers hebben een gedeeld Docker-netwerk nodig; dit is het netwerk van gluetun, aangezien de gekoppelde container zelf geen netwerk heeft. Hoe Docker Compose-netwerken zijn bedraad behandelt de standaardinstellingen.
Gebruik van buiten naar binnen de naam van gluetun en de poort waarop de applicatie luistert. Een reverse proxy-container bereikt de qBittorrent-webinterface via gluetun:8080. Hiervoor is geen ports:-vermelding nodig, omdat verkeer tussen containers op het Docker-netwerk blijft en nooit een hostpoort raakt.
Gebruik van binnen naar buiten de servicenaam van de andere container, bijvoorbeeld postgres:5432. Gluetun lost sinds v3.41 andere containernamen op vanuit zijn eigen namespace. Gebruik dus die versie of nieuwer als een naam niet wordt omgezet.
De firewall van gluetun bepaalt wie een verbinding mag openen. Verkeer vanaf het eigen Docker-netwerk van gluetun is toegestaan. Een client op een ander subnet, een laptop op uw LAN of een container op een afzonderlijk bridge-netwerk wordt geweigerd totdat u dat subnet benoemt:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24De gedocumenteerde betekenis is exact: door komma's gescheiden subnetten waartoe Gluetun en de containers die de netwerkstack delen, toegang hebben.
Inkomende verbindingen vanaf het internet vormen een apart probleem. Peers van een torrent-client komen binnen via de VPN-zijde, dus het publiceren van poort 6881 op de host heeft voor hen geen effect. U heeft een geforwarde poort van uw provider nodig, en die poort moet worden vermeld in FIREWALL_VPN_INPUT_PORTS, wat poorten vanaf de VPN-serverzijde toestaat. Dit is het onderdeel dat de meeste mediastacks gebouwd met Docker Compose niet correct configureren.
De kill switch: wat gebeurt er als de tunnel wegvalt
Dit patroon bewijst zijn waarde bij uitval. De gekoppelde container heeft geen tweede route. Het enige pad buiten de machine is de namespace die hij deelt; als de tunnel wegvalt, is er geen fallback-optie. De firewall van Gluetun dwingt dezelfde regel af vanaf de andere kant: uitgaand verkeer gaat via de tunnel of naar het VPN-server-eindpunt, al het overige wordt geweigerd. Er is geen moment waarop pakketten via de standaard interface lekken terwijl een client opnieuw verbinding maakt.
Gluetun bewaakt zijn eigen verbinding. Elke minuut verstuurt het een ICMP echo (een ping) naar de adressen in HEALTH_ICMP_TARGET_IPS, die standaard zijn ingesteld op 1.1.1.1,8.8.8.8. Elke vijf minuten voert het een volledige TCP en TLS (transport layer security) dial uit naar HEALTH_TARGET_ADDRESSES, standaard cloudflare.com:443,github.com:443. Wanneer deze falen, herstart het de VPN binnen de container en logt dit:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutLees de logs van de gekoppelde container met deze volgorde in gedachten. Regels zoals connection refused, operation not permitted en i/o timeout binnen de applicatie zijn gevolgen van een dode tunnel, geen oorzaken. De documentatie van Gluetun vermeldt dit expliciet, omdat gebruikers het gevolg rapporteren en daar urenlang naar zoeken.
HEALTH_RESTART_VPN=on is de standaardinstelling en moet ingeschakeld blijven. Schakel deze alleen uit tijdens het debuggen van één specifieke fout, want zonder deze optie blijft een dode tunnel dood.
Volgorde: voorkomen dat de stack start voordat de tunnel actief is
De image bevat een Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckDit commando voert een tweede, kortstondige kopie van gluetun uit die de health server van de actieve instantie op http://127.0.0.1:9999/ bevraagt. Een werkende tunnel antwoordt met 200 OK. Een defecte tunnel antwoordt met 500 Internal server error inclusief een foutmelding, waarna de container na één mislukking als ongezond (unhealthy) wordt gemarkeerd.
condition: service_healthy is de instelling die hierop wacht. Een standaard depends_on: [gluetun] wacht alleen tot de container is gestart; dit gebeurt enkele seconden voordat de handshake is voltooid. Hierdoor start de applicatie in een dood netwerk en geeft deze vaak op bij de eerste verbindingspoging. Healthchecks in Docker Compose behandelt de syntaxis en de timing-velden.
Eén beperking zorgt vaak voor verwarring. Compose evalueert deze voorwaarde slechts één keer, op het moment dat de container wordt aangemaakt. Het stopt of herstart de applicatie niet als gluetun later ongezond wordt. De interne auto-healing van gluetun vangt dit scenario op; daarom herstart deze het VPN-proces in plaats van de container.
Controleer op een DNS-lek voordat u de configuratie vertrouwt
DNS (domain name system) is het lek dat zelfs bij een correcte tunnel blijft bestaan. Gluetun draait een eigen resolver binnen de namespace en stuurt queries standaard door via DoT (DNS over TLS) naar Cloudflare: DNS_UPSTREAM_RESOLVER_TYPE=dot en DNS_UPSTREAM_RESOLVERS=cloudflare. Wijzig deze instellingen niet; uw opzoekingen zijn dan versleuteld en verlopen via de tunnel.
De instelling die dit proces verstoort is DNS_UPSTREAM_PLAIN_ADDRESSES. Gebruikers grijpen hiernaar wanneer een naam niet wordt omgezet en zij willen dat hun router of de resolver van hun provider het antwoord geeft. De documentatie van Gluetun is hierover duidelijk: al het DNS-verkeer gaat dan niet door de VPN-tunnel en lekt naar buiten. Uw dataverkeer blijft privé, maar uw lijst met opgevraagde hostnames niet. De WireGuard-versie van deze fout wordt behandeld in DNS die stopt met resolven via een WireGuard-tunnel.
Om dit te testen, stelt u HTTPPROXY=on in op gluetun en publiceert u 8888:8888/tcp. Wijs vervolgens een browser naar die proxy en voer een DNS-lektest uit. Het resultaat moet uw provider of Cloudflare tonen, nooit uw eigen router. De documentatie van Gluetun waarschuwt dat sommige lektesten vreemde resultaten rapporteren, omdat de resolver in de namespace een lokale caching-tussenpersoon is en niet de server die het uiteindelijke antwoord geeft. Beschouw een onjuist land of de resolver van uw eigen ISP als een duidelijk signaal voor een lek.
Tailscale toevoegen naast de VPN-sidecar, en welke voorrang krijgt
Tailscale is een overlay-netwerk gebouwd op WireGuard om uw eigen machines te bereiken. Gebruikers draaien dit vaak naast een provider-VPN om een beheerpad naar de stack te behouden. De twee conflicteren zelden, en daar is een reden voor. De documentatie van Tailscale stelt de standaard: het fungeert als een overlay-netwerk, het routeert alleen verkeer tussen apparaten waarop Tailscale draait, en het raakt uw openbare internetverkeer niet aan.
Het antwoord hangt dus af van één instelling.
- Tailscale in zijn eigen container, standaardconfiguratie: het ziet het uitgaande verkeer van de app nooit. Gluetun verwerkt al het verkeer. Tailscale bereikt de app op
gluetun:8080, precies zoals elke andere externe container. - Tailscale gekoppeld aan de namespace van gluetun met
network_mode: "service:gluetun": het heeft zijn eigencap_addvannet_adminennet_rawnodig, omdat capabilities niet automatisch meekomen met de namespace. In de standaard userspace-netwerkmodus staatTS_USERSPACEaan; tailscaled maakt geen interface aan en werkt als een SOCKS5- of HTTP-proxy, waardoor het de routing niet kan wijzigen. Gluetun verwerkt nog steeds alles. - Hetzelfde, met
TS_USERSPACE=false: tailscaled maakt een tunnel-device aan en installeert routes, maar alleen voor het tailnet-bereik100.64.0.0/10plus eventuele subnet-routes die u adverteert metTS_ROUTES. Openbaar verkeer verlaat het netwerk nog steeds via gluetun. - Elke bovenstaande optie met een geselecteerde exit node,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale claimt de standaardroute en krijgt voorrang. Combineer dit niet met gluetun. Er is slechts één standaardroute en één eigenaar.
Een bijwerking is zichtbaar wanneer Tailscale binnen de tunnel draait. De peers zien het adres van de VPN-provider, dus verwacht dat het vaker terugvalt op relays. tailscale status toont relay "..." naast een peer in plaats van direct wanneer dit is gebeurd. De verbinding werkt, maar is trager. Als het overlay-netwerk het enige is dat u daadwerkelijk nodig had, is het verschil tussen standaard WireGuard en Tailscale het betere startpunt.
Wat er misgaat en de melding die u zult zien
Docker weigert de app-container aan te maken. Error response from daemon: conflicting options: port publishing and the container type network mode betekent dat een ports:-blok nog steeds op de gekoppelde service staat. Verplaats dit naar gluetun.
Compose weigert het volledige bestand. Een service kan niet tegelijkertijd network_mode en networks instellen. Plaats de netwerken op gluetun.
Een andere container kan de app niet resolven. curl: (6) Could not resolve host: qbittorrent is het verwachte gedrag, omdat de gekoppelde container geen netwerk heeft toegevoegd en geen naam heeft geregistreerd. Gebruik gluetun en de poort.
De tweede gekoppelde container start niet. Twee processen in dezelfde namespace kunnen niet dezelfde poort binden; de verliezende partij meldt dat het adres al in gebruik is. Wijzig de interne poort van de app of start een tweede gluetun.
De app heeft geen netwerk nadat u gluetun heeft aangepast. Het herstarten of opnieuw aanmaken van gluetun verbreekt de verbinding voor alles wat eraan gekoppeld is. Herstart die containers.
Kleine pagina's laden, maar grote pagina's blijven hangen. Dit is de MTU (maximum transmission unit). De tunnel voegt overhead toe en een onderdeel in het pad verwijdert de te grote pakketten zonder een foutmelding terug te sturen. Verlaag WIREGUARD_MTU, probeer 1400 en daarna 1320.
Gluetun wordt niet 'healthy'. De opstartcontrole benoemt de eerste verdachten: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. Controleer of de sleutel is verlopen, of de serverlijst verouderd is en of uw host-firewall uitgaand UDP blokkeert.
FAQ
Waarom werken de gepubliceerde poorten van mijn container niet meer achter Gluetun?
Omdat network_mode: "service:gluetun" de container in de netwerk-namespace van Gluetun plaatst, en een namespace heeft slechts één IP-adres en één set luisterende poorten. De applicatie blijft luisteren, maar de publish-regel moet aanwezig zijn op de container die de namespace beheert. Verplaats de ports:-lijst naar de gluetun-service. Als u deze op de gekoppelde service laat staan, zal Docker deze niet eens aanmaken: Error response from daemon: conflicting options: port publishing and the container type network mode.
Hoe bereik ik een container binnen de VPN-tunnel vanuit een container daarbuiten?
Gebruik de servicenaam van gluetun en de poort waarop de applicatie luistert, bijvoorbeeld gluetun:8080. De gekoppelde container bevindt zich op geen enkel eigen Docker-netwerk, dus de eigen naam wordt nooit omgezet. Voor verkeer tussen containers hoeft niets te worden gepubliceerd. Andersom bereikt een container binnen de namespace een externe container via de servicenaam, zoals postgres:5432, op Gluetun v3.41 en nieuwer. Een client op een ander subnet, zoals een laptop op uw LAN, wordt door de firewall van gluetun geblokkeerd totdat u dat subnet toevoegt aan FIREWALL_OUTBOUND_SUBNETS.
Werkt Gluetun als een kill switch wanneer de VPN wegvalt?
Ja, en wel om twee redenen tegelijk. De gekoppelde container heeft geen andere route dan die in de gedeelde namespace, dus een weggevallen tunnel laat de container zonder pad buiten de machine achter. De firewall van Gluetun staat bovendien alleen uitgaand verkeer toe via de tunnel en naar het VPN-server-eindpunt. Gluetun herstart de VPN vervolgens intern, waarbij WARN [vpn] restarting VPN because it failed to pass the healthcheck wordt gelogd, in plaats van af te sluiten, omdat elke gekoppelde container zijn netwerk verliest wanneer gluetun zelf herstart.
Tailscale en Gluetun in dezelfde stack: welke verzorgt het uitgaande verkeer?
Gluetun, in elke configuratie behalve één. Tailscale routeert standaard alleen verkeer tussen apparaten in uw tailnet en laat openbaar verkeer ongemoeid. In de standaard userspace-modus van de container-image wordt er helemaal geen interface aangemaakt, dus kan het de routering niet beïnvloeden. Met TS_USERSPACE=false installeert het alleen routes voor 100.64.0.0/10 en uw geadverteerde subnetten. De uitzondering is een exit node: sudo tailscale set --exit-node=<exit-node-ip> maakt van Tailscale de standaardroute, en dan wint Tailscale. Kies één product om de standaardroute te beheren in plaats van beide te stapelen.