11 min read

The Shielded Tunnel: Securing Your Traffic with Gluetun & Docker

Digitized Spartan style warrior, bearing shield and spear confidently.
Gluetun is your shield, and a VPN is your spear!Gluetun is your shield, and a VPN is your spear!

In my previous guides, we built the server and we built the automation stack. Now, we need to talk about Operational Security (OpSec).

I've been using various types of VPN tech personally and professionally for two decades by this point and they have assisted me greatly, in both respects. I consider VPNs to be crucial as a tool for online security & privacy, and simply to access what I need at home. Especially when I am travelling or otherwise living a bit as a "digital nomad".

Whether you are sailing the high seas of the internet or just want to keep your ISP's nose out of your business, a VPN is non-negotiable. But running a VPN on your entire server is messy. It breaks remote access, screws with DNS, confuses Plex & Jellyfin, and adds latency where you don't want it. We're going to carve up your homelab with a scalpel, not bash it with a sledge hammer.

The solution? Split-Tunneling via Docker.

We are going to use Gluetun, a lightweight, "Swiss Army Knife" VPN client container. It acts as a secure gateway. We will configure specific containers (like your download clients) to route through Gluetun, while the rest of your server stays fast and directly connected.

GitHub - qdm12/gluetun: VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in.
VPN client in a thin Docker container for multiple VPN providers, written in Go, and using OpenVPN or Wireguard, DNS over TLS, with a few proxy servers built-in. - qdm12/gluetun

This guide covers:

  1. Setting up the Gluetun container.
  2. Configuration examples for NordVPN, PIA, and Mullvad.
  3. How to "attach" other containers (NZBGet, qBittorrent, etc.) to the tunnel.

πŸ› οΈ Step 1: The Master Container (Gluetun)

Gluetun is unique. It doesn't just connect to a VPN; it acts as a network supervisor. If the VPN connection drops, Gluetun acts as a "Kill Switch," instantly cutting networking to any container attached to it. No leaks. Ever.

Additionally there's a LOT of settings you can configure regarding DNS for Gluetun, but that's more advanced and might be worth a part 2. General rule of thumb if you have DNS issues - check to see which DNS resolver you have set in your system with cat /etc/resolv.conf. Full explanation of DNS settings for Gluetun here. Notice the defaults first. Don't change anything here unless you need to.

Here's a logical diagram of Gluetun in action -

flowchart TB

    %% ==========================
    %% 1. Local Network
    subgraph Local ["🏠 Home / Local Network"]
        Laptop["πŸ’» User / Laptop"]
        Router["🧱 EDGE Router / Firewall"]
    end

    %% ==========================
    %% 2. Docker Host & Gluetun Stack
    subgraph Host ["🐳 Docker Host Server"]

        %% Gluetun Namespace
        subgraph VPN_Stack ["πŸ›‘οΈ Gluetun Network Namespace"]
            Gluetun["πŸ›‘οΈ Gluetun Container"]

            %% Apps routed through Gluetun
            subgraph Apps ["Protected Apps"]
                qBit["⬇️ qBittorrent"]
                Prowl["πŸ” Prowlarr"]
                NZB["πŸ“¦ NZBGet"]
            end
        end
    end

    %% ==========================
    %% 3. The Tunnel
    subgraph Tunnel ["πŸ”’ The Encrypted Tunnel"]
        Nord["πŸ›‘οΈ VPN Provider (Nord / PIA / Mullvad)"]
    end

    %% ==========================
    %% 4. The Internet
    subgraph Internet ["🌍 The Public Internet"]
        ISP["🏒 ISP (Rogers / Bell)"]
        Swarm["🐝 Torrent Swarm"]
        Indexers["πŸ—‚οΈ Usenet / Trackers"]
    end

    %% ==========================
    %% CONNECTIONS (Top-Down Flow)

    Laptop -->|"WebUI Ports 8080 / 9696"| Gluetun
    Gluetun -.->|"Internal Port Mapping"| Apps

    Apps ==>|"All Traffic"| Gluetun
    Gluetun ==>|"WireGuard / OpenVPN"| Nord

    Nord ==>|"Masked IP"| Swarm
    Nord ==>|"Masked IP"| Indexers

    %% Kill Switch Logic
    Gluetun -.->|"β›” BLOCKED (Kill Switch)"| ISP
    Apps -.->|"β›” NO DIRECT ACCESS"| Router

    %% ==========================
    %% STYLING

    style Laptop fill:#f77f00,stroke:#d95d39,stroke-width:2px,color:#111
    style Router fill:#457b9d,stroke:#1d3557,stroke-width:2px,color:#fff

    style Gluetun fill:#0033a0,stroke:#001f66,stroke-width:4px,color:#fff
    style qBit fill:#d62828,stroke:#a71d2a,stroke-width:2px,color:#fff
    style Prowl fill:#d62828,stroke:#a71d2a,stroke-width:2px,color:#fff
    style NZB fill:#d62828,stroke:#a71d2a,stroke-width:2px,color:#fff

    style Nord fill:#0033a0,stroke:#001f66,stroke-width:4px,color:#fff

    style ISP fill:#555,stroke:#333,stroke-width:2px,color:#fff,stroke-dasharray: 5 5
    style Swarm fill:#ffcc00,stroke:#996600,stroke-width:2px,color:#111
    style Indexers fill:#ffcc00,stroke:#996600,stroke-width:2px,color:#111

    style Local fill:transparent,stroke:#888,stroke-width:1px,stroke-dasharray:4 4
    style Host fill:#f1f1f1,stroke:#333,stroke-width:2px,color:#333
    style VPN_Stack fill:#e0e7ff,stroke:#0033a0,stroke-width:2px,stroke-dasharray:5 5,color:#0033a0
    style Tunnel fill:transparent,stroke:#0055aa,stroke-width:3px,stroke-dasharray:2 2
    style Internet fill:transparent,stroke:#aa0000,stroke-width:1px,stroke-dasharray:4 4

The Base Compose File

Here is the base docker-compose structure. You will choose ONE of the provider blocks below to fill in the environment variables. Their full list of compatible providers is here.

Create a new directory (e.g., /opt/appdata/gluetun or /opt/DOCKERS/gluetun) and a docker-compose.yml file.

version: "3"
services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      # --- WEB UI PORTS ONLY ---
      # We only map ports that YOU need to access from your LAN.
      # We do NOT map torrent ports (like 6881) here because 
      # incoming VPN traffic is handled by the VPN provider, not Docker.
      - "8080:8080" # qBittorrent Web UI
      - "9696:9696" # Prowlarr Web UI
      - "6789:6789" # NZBGet Web UI
    volumes:
      - /opt/appdata/gluetun:/gluetun
    environment:
      - TZ=America/Toronto
      
      # --- VPN SETTINGS ---
      # See full list of providers here: 
      # https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=openvpn
      - OPENVPN_USER=your_service_username
      - OPENVPN_PASSWORD=your_service_password
      - SERVER_REGIONS=Canada
      #- FIREWALL_VPN_INPUT_PORTS=21754 #Only if specified by your VPN provider
      #- FIREWALL_OUTBOUND_SUBNETS=10.0.0.0/16 #Only if you require a docker using Gluetun to have access to the outside world NOT using the VPN!
      
    restart: always
πŸ’‘
Note on FIREWALL_OUTBOUND_SUBNETS= If you have prowlarr in gluetun but not sonarr, you need this so prowlarr can reach sonarr. It's like outbound access FROM Gluetun, to everything else, outside the VPN.

See: https://www.reddit.com/r/gluetun/comments/1p7htlb/comment/nqxyrl9/

Alternatively, you can simply add it to your existing docker-compose.yml if you'd prefer.

πŸ’₯
You can (and should) put the VPN secrets into your docker compose .env file if you already have one. If you don't, now is a great time to switch over! Good example here: https://www.reddit.com/r/gluetun/comments/1kpbfs2/the_definitive_howto_for_setting_up_protonvpn/

Compose Example with .env variables & Wireguard

services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- 8080:8080/tcp # qbittorrent
environment:
- TZ=${TZ}
- UPDATER_PERIOD=24h
- VPN_SERVICE_PROVIDER=protonvpn
- VPN_TYPE=${VPN_TYPE}
- BLOCK_MALICIOUS=off
- OPENVPN_USER=${OPENVPN_USER}
- OPENVPN_PASSWORD=${OPENVPN_PASSWORD}
- OPENVPN_CIPHERS=AES-256-GCM
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- PORT_FORWARD_ONLY=on
- VPN_PORT_FORWARDING=on
- VPN_PORT_FORWARDING_UP_COMMAND=/bin/sh -c 'wget -O- --retry-connrefused --post-data "json={\"listen_port\":{{PORTS}}}" http://127.0.0.1:8080/api/v2/app/setPreferences 2>&1'
- SERVER_COUNTRIES=${SERVER_COUNTRIES}
volumes:
- ${MEDIA_DIR}/gluetun/config:/gluetun
restart: unless-stopped

qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
depends_on:
gluetun:
condition: service_healthy
environment:
- PUID=1000
- PGID=1000
- TZ=${TZ}
- WEBUI_PORT=8080
volumes:
- ${MEDIA_DIR}/qbittorrent/config:/config
- ${MEDIA_DIR}/qbittorrent/downloads:/downloads
restart: unless-stopped
network_mode: "service:gluetun"

.env here:
# Fill in either the OpenVPN or Wireguard sections. The choice of vpn is made with VPN_TYPE. Choose 'wireguard' or 'openvpn'. The settings for the other vpn type will be ignored.
# Alter the TZ, MEDIA_DIR, and SERVER_COUNTRIES to your preference. Run 'docker run --rm -v eraseme:/gluetun qmcgaw/gluetun format-servers -protonvpn' to get a list of server countries

# Base config
TZ=Australia/Brisbane
MEDIA_DIR=/media

# Gluetun config
VPN_TYPE=wireguard #openvpn
SERVER_COUNTRIES=Albania,Algeria,Angola,Argentina,Australia,Austria,Azerbaijan

# OpenVPN config
OPENVPN_USER=username+pmp
OPENVPN_PASSWORD=password

# Wireguard config (example key)
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=

From: https://www.reddit.com/r/gluetun/comments/1kpbfs2/comment/mswnynw/


πŸ”Œ Provider Configurations

Copy the block for your specific provider and paste it into the environment section above. Order is in simple list, not order of preference. I personally use PIA.

Option A: NordVPN (OpenVPN)

Note: While Nord supports WireGuard (NordLynx), extracting the private key is complex. OpenVPN is the easiest setup.

      - VPN_SERVICE_PROVIDER=nordvpn
      - OPENVPN_USER=service_user
      - OPENVPN_PASSWORD=service_pass

Option B: Private Internet Access (PIA)

PIA works excellently with Gluetun and supports port forwarding automation.

      - VPN_SERVICE_PROVIDER=private internet access
      - OPENVPN_USER=p1234567
      - OPENVPN_PASSWORD=password
      - VPN_PORT_FORWARDING=on # Gluetun will automate the port assignment!

Option C: Mullvad (WireGuard)

Mullvad is the privacy gold standard and works natively with WireGuard in Gluetun for blazing speeds.

      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=your_key
      - WIREGUARD_ADDRESS=10.xx.xx.xx/32

πŸ”— Step 2: Routing Your "Bad" Traffic

Now for the magic. We generally only route Download Clients (qBittorrent, NZBGet, SABnzbd) or Index Managers (Prowlarr) through the VPN.

Do you need to route Sonarr/Radarr? Generally, no.

  • The Traffic is Safe: Sonarr and Radarr communicate with indexers via HTTPS. This means your traffic is encrypted; your ISP can see you are talking to a tracker, but they cannot see what you are searching for or downloading (the .torrent file). Mind you this is why I prefer using magnet links...
  • The Risk is Low: You aren't transferring the copyrighted content itself through these apps; you're just grabbing metadata.
  • The Complexity: Routing them through the VPN can make it harder for them to talk to your download clients if you aren't careful.

Torrenting is the risk. The actual peer-to-peer transfer in qBittorrent is often unencrypted and easily visible to ISPs and copyright trolls. This is what must go through the tunnel.

How to Connect a Container to Gluetun

We use the network_mode directive in Docker. This tells a container: "Don't use your own network card. Plug directly into Gluetun's network card instead."

  1. Remove Ports: You must remove the ports: section from your app container (e.g., qBittorrent). Since it doesn't have its own network anymore, it can't open its own ports.
  2. Move Ports: Ensure those Web UI ports (e.g., 8080 for qBit) are listed in the Gluetun container's ports: section instead.
  3. Add Network Mode: Add the line network_mode: service:gluetun

Example: Routing NZBGet through Gluetun

  nzbget:
    image: lscr.io/linuxserver/nzbget:latest
    container_name: nzbget
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Toronto
    volumes:
      - /opt/appdata/nzbget:/config
      - /data/downloads:/downloads
    # OLD WAY:
    # ports:
    #   - 6789:6789
    
    # NEW WAY (The Secure Way):
    network_mode: service:gluetun 
    # This tells Docker: "Don't give this container a network. 
    # Use Gluetun's network stack instead."

Example: Routing QBittorrent through Gluetun

qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    # ... environment and volumes ...
    
    # ❌ OLD WAY (Delete this section):
    # ports:
    #   - 8080:8080
    
    # βœ… NEW WAY (The Secure Way):
    network_mode: service:gluetun 
    depends_on:
      gluetun:
        condition: service_healthy

For more details on various ways to connect dockers in the same compose or in a different compose, read up here:

gluetun-wiki/setup/connect-a-container-to-gluetun.md at main Β· qdm12/gluetun-wiki
Home to the Markdown Wiki page for Gluetun. Contribute to qdm12/gluetun-wiki development by creating an account on GitHub.

Putting it all Together - Full Stack

version: "3"
services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      # --- WEB UI PORTS ONLY ---
      # We only map ports that YOU need to access from your LAN.
      # We do NOT map torrent ports (like 6881) here because 
      # incoming VPN traffic is handled by the VPN provider, not Docker.
      - "8080:8080" # qBittorrent Web UI
      - "9696:9696" # Prowlarr Web UI
      - "6789:6789" # NZBGet Web UI
    volumes:
      - /opt/appdata/gluetun:/gluetun
    environment:
      - TZ=America/Toronto
      
      # --- VPN SETTINGS ---
      # See full list of providers here: 
      # https://github.com/qdm12/gluetun-wiki/tree/main/setup/providers
      - VPN_SERVICE_PROVIDER=nordvpn
      - VPN_TYPE=openvpn
      - OPENVPN_USER=your_service_username
      - OPENVPN_PASSWORD=your_service_password
      - SERVER_REGIONS=Canada
      
      # --- NETWORK PERMISSIONS (Optional) ---
      # Only uncomment this if your containers inside the VPN (e.g. Prowlarr)
      # need to talk to services OUTSIDE the VPN (e.g. Sonarr on your LAN).
      # - FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
      
    restart: always

  # -------------------------------------------------------
  # QBITTORRENT (Attached to Gluetun)
  # -------------------------------------------------------
  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=America/Toronto
      - WEBUI_PORT=8080
    volumes:
      - /opt/appdata/qbittorrent:/config
      - /data/downloads:/downloads
    network_mode: service:gluetun
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

πŸ”‘ Key Configuration Details

  1. Port Moving is Mandatory: If you try to keep ports: 8080:8080 inside the qBittorrent service, Docker will throw an error. You are effectively unplugging qBittorrent's network cable and plugging it into Gluetun. Therefore, Gluetun becomes the only door in or out. All Web UI ports must be declared in the Gluetun service.
  2. FIREWALL_OUTBOUND_SUBNETS (The Common Myth): You might see guides claiming you need this setting to access the Web UI from your browser. This is false.
    • Inbound is Automatic: Docker handles the inbound traffic from your browser to the Web UI via the port mapping. You don't need extra firewall rules for this.
    • When DO you need it? You only need to define FIREWALL_OUTBOUND_SUBNETS if the container inside the VPN needs to initiate a connection to something outside on your LAN.
    • Example: If Prowlarr is inside the VPN, but Sonarr is running normally on your LAN IP (192.168.1.50), Prowlarr needs this setting to be allowed to "reach out" and talk to Sonarr.
  3. NordVPN Credentials:
    • Do NOT use your regular email/password. NordVPN (and many others) use special "Service Credentials" for manual setups like this.
    • Find them in your Nord Account dashboard under Set up NordVPN manually.
  4. Verification: You don't need complex commands to see if it works. Just check the logs! Run docker logs gluetun. If successful, you will see a message confirming the connection and displaying the Public IP assigned by the VPN provider. If you see this IP, your traffic is secure.

⚑ WireGuard Setup vs. OpenVPN

The example above uses VPN_TYPE=openvpn because it is easiest to set up with just a username/password. For details on WG in Gluetun, read here:

The definitive HOWTO for setting up ProtonVPN, Gluetun, and Qbittorernt with fully automated port forwarding.
by u/sboger in gluetun

If you want faster speeds with WireGuard, change these lines in the Gluetun section:

      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=your_extracted_private_key
      # Remove OPENVPN_USER and OPENVPN_PASSWORD

Note: Getting the WireGuard private key from NordVPN requires a few extra manual steps (usually running a quick script on a Linux machine), as they don't show it plainly in the dashboard.

Also head over to this thread for the latest details on WG setup which require some work


πŸ•΅οΈ Step 3: Verification (The "Is it Working?" Test)

Forget about running complex terminal commands inside containers. Gluetun is smart; it checks its own connection status and tells you the result.

To verify your connection, simply check the container logs:

docker logs gluetun
# Or to see live running log:
docker logs -f gluetun

What to look for: Scroll to the bottom. If the connection is successful, you will see a message explicitly stating your new Public IP.

INFO [ip getter] Public IP address is 185.xxx.xxx.xxx (Region: Toronto, CA)
  • If you see an IP that is NOT your home IP: Success! You are secure.
  • If you see errors or "unhealthy": The container will likely restart or stop. Because we used network_mode: service:gluetun, if Gluetun isn't healthy, your apps literally have no network connection. The "Kill Switch" is built-in by design.

πŸ›‘οΈ A Note on the "Kill Switch"

You don't need to configure a separate kill switch. By removing the network interface from your app (qBittorrent) and plugging it into Gluetun, you have created a physical dependency.

  • Gluetun Up = Internet Access via VPN.
  • Gluetun Down = No Network Card = Zero Traffic.

It is physically impossible for qBittorrent to leak traffic over your normal ISP connection because it no longer has access to that hardware interface.


Secure your traffic, protect your privacy, and sail safely. πŸš€πŸ›‘οΈ

The Ultimate Self-Hosted Media Server Guide
We’ve all felt it. You subscribe to three different streaming services, and the one show you want to watch is on a fourth. You’re paying a premium for a fragmented, inconvenient experience. Or maybe you have a vast local library of media, but it’s a mess of folders on a