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
Shellbash
Updated
Script
bash
# 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.

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.

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.