LinuxcontainersTested on real hardware

VPN-Shielded Torrent Stack (gluetun + qBittorrent)

Run qBittorrent inside a gluetun VPN container so all torrent traffic exits through a VPN with a kill-switch — plus port-forward sync for full download speed.

Distrosubuntu, debian
Shellyaml
Updated
Compose file
yaml
# docker-compose.yml — qBittorrent routed entirely through gluetun's VPN.
# qBittorrent shares gluetun's network namespace, so if the VPN drops,
# the torrent client loses all connectivity (kill-switch by design).
services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add: [NET_ADMIN]
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "8080:8080"        # qBittorrent WebUI (published by gluetun)
    environment:
      - VPN_SERVICE_PROVIDER=protonvpn
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=YOUR_WIREGUARD_PRIVATE_KEY
      - SERVER_COUNTRIES=Netherlands
      - VPN_PORT_FORWARDING=on
      - PORT_FORWARD_ONLY=on
      # Let the client reach LAN services (e.g. your *arr apps)
      - FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"   # <-- all traffic through the VPN
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Etc/UTC
      - WEBUI_PORT=8080
    volumes:
      - /opt/qbittorrent/config:/config
      - /srv/downloads:/downloads
    depends_on:
      - gluetun
    restart: unless-stopped

What this does

Runs qBittorrent inside a gluetun VPN container so every byte of torrent traffic exits through your VPN — never your home IP. Because qBittorrent shares gluetun’s network namespace (network_mode: service:gluetun), if the VPN tunnel drops the client instantly loses all connectivity: a built-in kill-switch, not a bolt-on. The WebUI is still reachable on your LAN because gluetun publishes the port.

This is the download client that feeds a Plex library. Pair it with the ClamAV scan-and-purge playbook to keep the download folder clean.

Why this approach

Running a torrent client without a VPN exposes your home IP to every peer you connect with, which is logged by both copyright monitoring organizations and your ISP. Whether or not you’re downloading anything you shouldn’t be, this is a privacy exposure. A VPN routes all torrent traffic through a server in another location, so the logged IP is the VPN exit node, not your home.

The gluetun container approach is better than configuring a VPN connection directly inside qBittorrent (which supports SOCKS5 proxies) because it operates at the network namespace level, not the application level. When qBittorrent uses network_mode: service:gluetun, it shares gluetun’s entire network stack — every packet it sends goes through the tunnel. There’s no configuration in qBittorrent that can accidentally bypass the VPN, no bind-address setting to get wrong, and no risk that an application-layer leak sends traffic outside the tunnel. If gluetun stops, qBittorrent has no network at all.

Port forwarding deserves a mention because it significantly affects download speeds. BitTorrent is a peer-to-peer protocol where peers can either dial you (incoming connection) or be dialed by you (outgoing). Without port forwarding through the VPN, only outgoing connections work — you can connect to other peers but they can’t connect to you. Most VPN providers that support port forwarding (ProtonVPN, Mullvad, AirVPN, PIA among others) offer it as an option. Without it, download speeds are typically 30–50% of what they’d be with it, depending on the swarm.

Prerequisites

  • Docker + Docker Compose (install guide)
  • A VPN provider that supports WireGuard and port forwarding (port forwarding is what gets you full download speed — without it, incoming peer connections are blocked)
  • Your WireGuard private key and preferred server country from the provider
  • Adjust 192.168.1.0/24 to your real LAN subnet, and paths to taste

Deploy

Save the script above as /opt/arr-vpn/docker-compose.yml, fill in your key, then:

bash

sudo mkdir -p /opt/qbittorrent/config /srv/downloads
cd /opt/arr-vpn
sudo docker compose up -d

# Confirm traffic exits the VPN, NOT your home WAN:
sudo docker exec gluetun wget -qO- https://api.ipify.org ; echo

That IP must be your VPN provider’s, not your ISP’s. If it’s your home IP, stop — the tunnel isn’t up and nothing should be downloading yet.

This one-time check is worth automating so a post-reboot leak can’t go unnoticed. Have the host fetch its own public IP each run and compare it to the client’s exit IP — if they match, alert. That auto-detects your current WAN address instead of hardcoding a home IP that goes stale; the pattern is in The Self-Healing Arr Stack.

The qBittorrent WebUI is at http://<host-ip>:8080. The linuxserver image prints a temporary admin password in its logs on first start:

bash

sudo docker logs qbittorrent 2>&1 | grep -i password

Port-forward sync

Many VPN providers hand you a forwarded port that rotates. qBittorrent needs to listen on the current one for full speed. gluetun writes the active port to a file; sync it into qBittorrent on a schedule.

Save as /opt/arr-vpn/qbit-portsync.sh and cron it every 5 minutes:

bash

#!/usr/bin/env bash
# Push gluetun's current forwarded port into qBittorrent.
set -euo pipefail
QBIT="http://127.0.0.1:8080"
USER="admin"
PASS="YOUR_QBIT_PASSWORD"

PORT=$(sudo docker exec gluetun cat /tmp/gluetun/forwarded_port)

# qBittorrent 5.x returns HTTP 204 (empty body) on a successful login
COOKIE=$(curl -s -i --data "username=$USER&password=$PASS" \
"$QBIT/api/v2/auth/login" | grep -i '^set-cookie' | sed 's/.*SID=\([^;]*\).*/\1/')

curl -s --cookie "SID=$COOKIE" \
--data "json={\"listen_port\":$PORT}" \
"$QBIT/api/v2/app/setPreferences"

echo "synced listen_port=$PORT"
bash

# crontab -e
*/5 * * * * /opt/arr-vpn/qbit-portsync.sh >> /var/log/qbit-portsync.log 2>&1

Notes

  • Verify the kill-switch: stop gluetun (docker stop gluetun) and confirm qBittorrent loses network. It shares the namespace, so it must go dark.
  • FIREWALL_OUTBOUND_SUBNETS is what lets qBittorrent (and anything else in the namespace) still reach LAN services like Radarr/Sonarr while everything else is forced through the VPN. Set it to your real subnet.
  • Route Prowlarr behind the same VPN for full metadata shielding by adding network_mode: service:gluetun to it too, and publishing its port on the gluetun service. Note that some CloudFlare-protected indexers block datacenter/VPN exit IPs — prefer indexers that work from a VPN exit.
  • Don’t recreate the stack while downloads are active unless you have to — recreating gluetun restarts qBittorrent and pauses in-flight torrents.
  • qBittorrent 5.x login quirk: a successful login returns HTTP 204 with an empty body (not the string “Ok.”), and repeated failed logins trigger a temporary per-IP auth ban — restart the container to clear it.
  • Never commit your real WireGuard key. Keep it in the compose file on the host only, or reference it from an .env file that’s outside version control.