Route Docker Containers Through a VPN with Gluetun (2026 Guide)

Route Docker Containers Through a VPN with Gluetun (2026 Guide)

Learn how to use Gluetun as a Docker VPN gateway to selectively route containers through a VPN while keeping others on a direct connection. No more leaking your IP from qBittorrent.

💡 Disclosure: This article contains affiliate links. If you make a purchase through these links, we may earn a small commission at no extra cost to you. This helps support the site and keeps the content free.

I run a media server. Jellyfin, Sonarr, Radarr, qBittorrent — the usual stack. For months, everything was on a direct connection. My server’s IP was doing the torrent handshake itself. Not great.

The naive solution? Put the whole server on a VPN. But then Jellyfin would break because my Plex/Jellyfin users can’t reach it through a VPN endpoint. My reverse proxy would stop resolving. My monitoring would go dark.

I needed something smarter: route only specific containers through a VPN. Everything else stays on the direct connection.

Enter Gluetun. It’s a lightweight Docker container that acts as a VPN client (WireGuard, OpenVPN, NordVPN, Mullvad, and 30+ providers) and lets other containers route their traffic through it via Docker networking.

I’ve been running this setup for 8 months. It works beautifully. Here’s exactly how to do it.

Why You Want This

If you’re self-hosting anything that touches P2P networks, geolocked services, or privacy-sensitive APIs, you need selective VPN routing. Here’s why:

Your IP is your identity. If qBittorrent or a P2P client connects from your home IP or your VPS’s raw IP, that IP is now public in swarm trackers. Forever. I learned this the hard way when my Hetzner IP ended up in a public DHT dump.

VPNs break direct access. Put the whole server behind a VPN and suddenly your Jellyfin is unreachable, your Caddy can’t issue certs, and your backup cronjob can’t reach S3. Selective routing fixes this.

Some providers block inbound traffic. Hetzner, OVH, and Contabo all get aggressive about DMCA notices. Routing torrent traffic through a VPN keeps those complaints in the VPN provider’s inbox, not yours.

The alternatives are worse. Running a VPN client inside each container? Inefficient and annoying to configure. Using network namespaces manually? Possible but error-prone. Gluetun gives you a clean Docker-native pattern that’s easy to maintain.

How Gluetun Works

Gluetun runs as a container that establishes a VPN connection to your provider. Other containers connect to it through Docker’s network_mode: "service:gluetun" setting, sharing its network stack. All their traffic flows through the VPN tunnel automatically.

The architecture is dead simple:

qBittorrent ─┐
Sonarr ──────┤──→ [gluetun container] ───→ VPN tunnel ───→ Internet
Jellyfin ────┘

Nextcloud ─────────────→ [direct internet]
Traefik ───────────────→ [direct internet]

Only the containers you explicitly attach to Gluetun go through the VPN. Everything else stays on your normal connection.

What You’ll Need

  • Docker and Docker Compose installed on your server
  • A VPN subscription that supports WireGuard or OpenVPN. I use NordVPN (affiliate link below), but Mullvad, ProtonVPN, PIA, and most others work too.
  • Your VPN credentials — API token, username/password, or config file depending on provider

I run this on a Hetzner CPX31 (4 vCPU, 8GB RAM). It handles Gluetun + 4 VPN-routed containers without breaking a sweat. The VPN overhead is minimal — Gluetun itself uses about 50-100MB RAM.

🚀NordVPN

I use NordVPN for my self-hosted stack. 30-day money-back guarantee.

Get NordVPN →

Affiliate link — we may earn a commission at no extra cost to you.

Step 1: Basic Gluetun Setup

Let me start with a clean docker-compose.yml that runs Gluetun with NordVPN (adjust for your provider).

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    environment:
      # Replace with your provider
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=wireguard
      # NordVPN credentials from your account dashboard
      - [email protected]
      - NORDVPN_PASSWORD=your-service-password
      # Optional: pick a specific server or region
      - SERVER_REGIONS=Netherlands
      - SERVER_COUNTRIES=Netherlands
    ports:
      # Expose ports for containers that need it
      # We'll add app-specific ports later
    networks:
      - gluetun-net
    restart: unless-stopped

networks:
  gluetun-net:
    driver: bridge

One gotcha right away: cap_add: [NET_ADMIN] is required. Without it, Gluetun can’t modify routing tables or open the VPN tunnel. Docker doesn’t grant this by default for good reason — but Gluetun needs it.

Start it up:

docker compose up -d

Check if the VPN connected:

docker logs gluetun | grep -i "connected"

You should see something like “WireGuard is up” or “connection established”. If not, run:

docker logs gluetun

Most failures are wrong credentials or provider config. Double-check your VPN provider’s docs for the exact VPN_SERVICE_PROVIDER and credential format. NordVPN uses a dedicated “service password” (not your main account password) — that one tripped me up for 20 minutes.

Step 2: Route a Container Through the VPN

Here’s the magic. To route qBittorrent through Gluetun, use network_mode: "service:gluetun":

services:
  gluetun:
    # ... (same as above)

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent/config:/config
      - /data/downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

Wait, how do I access the web UI if the container shares Gluetun’s network?

That’s the catch. When you use network_mode: "service:gluetun", the container shares Gluetun’s network stack — and Gluetun doesn’t expose any ports by default. You need to add qBittorrent’s ports to Gluetun’s ports section:

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    environment:
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=wireguard
      - [email protected]
      - NORDVPN_PASSWORD=your-service-password
    ports:
      - "8080:8080"    # qBittorrent web UI
      - "6881:6881"    # qBittorrent DHT
      - "6881:6881/udp"
    networks:
      - gluetun-net
    restart: unless-stopped

Now you can access qBittorrent at http://your-server-ip:8080. The traffic from qBittorrent goes through the VPN, but you can reach the web UI from your local machine because the port is mapped on the host.

I’ll be honest: this feels wrong at first. You’re putting port mappings on the Gluetun service, not the actual app. But it works, and once you get used to it, it’s clean — one place to manage all exposed ports for VPN-routed services.

Verify the VPN is Working

Before you trust it, verify that your container is actually routing through the VPN:

# Check the container's public IP via curl
docker exec -it gluetun curl -s ifconfig.me

If this returns your VPN provider’s IP (not your server’s IP), you’re good. Run it a few times to confirm it’s stable. I check mine weekly.

Step 3: Add Multiple VPN-Routed Services

Sonarr and Radarr follow the same pattern. Here’s the full stack:

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    environment:
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=wireguard
      - [email protected]
      - NORDVPN_PASSWORD=your-service-password
      - SERVER_COUNTRIES=Netherlands
    ports:
      - "8080:8080"    # qBittorrent
      - "8989:8989"    # Sonarr
      - "7878:7878"    # Radarr
      - "6881:6881"
      - "6881:6881/udp"
    networks:
      - gluetun-net
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent/config:/config
      - /data/downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
    volumes:
      - ./sonarr/config:/config
      - /data/tv:/tv
      - /data/downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Paris
    volumes:
      - ./radarr/config:/config
      - /data/movies:/movies
      - /data/downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

Notice a pattern? Every app that needs VPN routing gets network_mode: "service:gluetun" and depends_on: gluetun. Its ports get added to the Gluetun ports block. Everything else in your stack stays on its own Docker network.

This pattern scales well. I route 5 containers through Gluetun and it’s been running for months without a restart.

Step 4: Keep Other Containers Direct

This is where the approach really shines. Your non-VPN containers stay on their own networks, completely unaffected:

services:
  # ... gluetun, qbittorrent, sonarr, radarr from above

  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    volumes:
      - ./jellyfin/config:/config
      - /data/media:/media
    ports:
      - "8096:8096"
    restart: unless-stopped

  caddy:
    image: caddy:alpine
    container_name: caddy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./caddy/data:/data
      - ./caddy/Caddyfile:/etc/caddy/Caddyfile
    restart: unless-stopped

Jellyfin and Caddy don’t have network_mode: "service:gluetun". They connect directly to the internet. Caddy can issue SSL certs, Jellyfin can reach external metadata sources, and none of it leaks your IP because they’re not doing P2P traffic.

This separation is the whole point. You’re not choosing between “everything on VPN” or “nothing on VPN”. You get granular control.

What I Learned the Hard Way

Port Conflicts

If two VPN-routed containers both try to use port 8080 internally, you can’t map both to the same host port. Gluetun’s port mapping is per-container, and since they share its network, port conflicts become host conflicts.

Fix: Use different internal ports or use Docker’s extra_hosts to route between them. I changed qBittorrent’s WEBUI_PORT to 8081 and Sonarr to 8989.

VPN Drops

Gluetun has a built-in kill switch — if the VPN drops, it blocks all traffic. That’s great for privacy, but it means if your VPN provider has a hiccup at 3am, your entire VPN-routed stack goes offline until Gluetun reconnects.

Fix: Gluetun has VPN_DURATION=24h and OPENVPN_RENEG_SEC=0 options to auto-reconnect. I also monitor it with Uptime Kuma — if Gluetun goes down, I get a Discord ping.

DNS Leaks

By default, Gluetun uses the VPN provider’s DNS. But if you’ve got custom DNS (Pi-hole, AdGuard Home), you might leak queries. Gluetun supports custom DNS via the DNS_PLAINTEXT_ADDRESS environment variable. I point mine at my AdGuard Home instance:

environment:
  - DNS_PLAINTEXT_ADDRESS=192.168.1.10

Speed Overhead

WireGuard adds about 5-10% overhead on my 1Gbps connection. For downloads, I barely notice. For streaming, it’s negligible. OpenVPN is heavier — 15-20% overhead. If speed matters, use WireGuard.

Advanced: Firewall Rules with Gluetun

Gluetun supports custom firewall rules so you can restrict which containers can communicate through it. I use this to block everything except my media stack:

environment:
  - FIREWALL_INPUT_PORTS=6881
  - FIREWALL_OUTBOUND_SUBNETS=0.0.0.0/0

This only allows incoming on port 6881 (DHT) and allows all outbound traffic. You can lock it down further by restricting outbound subnets to specific tracker URLs.

Should You Do This?

If you:

  • Run any P2P or torrent client in Docker
  • Have services that need geolocation spoofing
  • Want privacy from your VPS provider for specific traffic
  • Need granular control over which containers use the VPN

Then yes, this setup is worth the 30-minute configuration.

If you only have one or two containers and don’t care about the others, a simpler approach might be running a VPN client inside each container. But Gluetun scales much better as your stack grows.

I moved from “VPN in every container” to Gluetun about 8 months ago. One container to manage, one place to check connection status, one set of logs to watch. No more credential duplication, no more sync issues when rotating VPN configs. Just clean service-level routing.

The Quick Recap

  1. Run Gluetun with your VPN provider credentials
  2. Add network_mode: "service:gluetun" to any container that needs VPN
  3. Expose their ports through Gluetun’s ports section
  4. Verify with curl ifconfig.me from inside the Gluetun container
  5. Keep non-VPN containers on their own networks — they’re unaffected

That’s it. You’ve got selective VPN routing without the complexity of network namespaces, without duplicate VPN clients, and without breaking everything else.


Related Articles:

Running on my Hetzner CPX31, routing qBittorrent + Sonarr + Radarr + Prowlarr through Gluetun → NordVPN. 8 months, zero leaks, zero DMCA notices.

Stay in the loop 📬

Get self-hosting tutorials, tool reviews, and infrastructure tips delivered to your inbox. No spam, unsubscribe anytime.

Join 0 self-hosters. Free forever.