A docker-compose stack that runs qBittorrent only when it's actually routed through the WireGuard tunnel - not "the container is up," but "the tunnel is genuinely passing traffic." If WireGuard drops, qBittorrent gets stopped automatically before it can leak or silently accept stalled jobs from Sonarr/Radarr/Prowlarr. When the tunnel comes back, it restarts itself. No manual babysitting, no separate uptime script, no cron jobs.
- WireGuard-gated lifecycle - qBittorrent only runs when the tunnel is actually passing traffic, verified independently rather than just trusted; stops within ~10-15s of a real failure, restarts automatically on recovery.
- Zero leak risk - qBittorrent has no network of its own, so there's no route to leak through even before the watchdog reacts.
- Automatic tracker refresh - pulls a fresh best-trackers list and pushes it to qBittorrent whenever it's confirmed alive.
- Doesn't blindly trust the Docker health badge - it's just the exit code of gluetun's own healthcheck command, not proof WireGuard is actually passing traffic (see below); the watchdog runs its own independent connectivity test through the tunnel instead of taking that badge's word for it.
- Zero babysitting - no manual restarts and no separate uptime checks required to keep this reliable (bring your own monitoring if you want alerts on top).
It's three containers:
gluetun- the WireGuard client. Owns the network. Tries to reconnect on its own when the tunnel drops, though not always successfully or promptly.qbittorrent- the torrent client. Has no network of its own - it rides entirely inside gluetun's network namespace.torrent-vpn-watchdog- a small watcher that ties qBittorrent's lifecycle to a verified connectivity check (not just gluetun's self-reported status), and also keeps your tracker list fresh.
The common gluetun + qBittorrent setup already prevents IP leaks (via network_mode: service:gluetun + gluetun's kill-switch firewall). What it doesn't solve on its own:
- WireGuard is a silent protocol - it doesn't report failures. Docker's "healthy" badge on the gluetun container doesn't know anything about WireGuard connection status while qBittorrent can't actually pass a single packet through the tunnel. It's the whole reason this stack runs its own independent connectivity test.
- If WireGuard drops mid-session, qBittorrent's WebUI often keeps running and keeps accepting API calls from your *arr stack - the job gets queued, then just sits at 0 peers with nothing telling you why.
- Nothing was watching gluetun's health and reacting to it.
This stack closes that loop.
flowchart LR
subgraph "Docker network"
G[gluetun<br/>WireGuard client]
Q[qbittorrent<br/>network_mode: service:gluetun]
W[torrent-vpn-watchdog]
end
VPN[(VPN Provider)]
ARR[Sonarr / Radarr / Prowlarr]
G <-->|WireGuard tunnel| VPN
Q -.->|shares network namespace<br/>zero traffic if tunnel is down| G
W -->|health status + exec-based<br/>connectivity test, every 10s| G
W -->|docker start / docker stop| Q
ARR -->|WebUI API<br/>port 8080, over LAN or same Docker network| G
Because qBittorrent has no network stack of its own, if the tunnel ever goes down, qBittorrent has zero route to the internet - not "an unprotected route," literally none. That's the leak protection, and it doesn't depend on anything in this repo working correctly. The watchdog is what makes the experience of a WireGuard drop clean (stopped client, no stale jobs, auto-recovery) rather than just safe-but-silent.
- Docker + Docker Compose v2 on a Linux host (mine runs on Synology/Portainer).
- A VPN provider that supports WireGuard and gives you a private key, address, and endpoint (Mullvad, ProtonVPN, Windscribe, NordVPN, AirVPN, and many others are supported by gluetun - see gluetun's wiki for provider-specific setup).
1. Get your WireGuard credentials from your VPN provider. Every provider does this differently - check the gluetun wiki for your specific provider's page. You need at minimum a private key and an address; some providers also need a preshared key.
2. Create your project folder and drop in the compose file:
mkdir -p torrent-stack/gluetun torrent-stack/qbittorrent/config torrent-stack/qbittorrent/data
cd torrent-stack
curl -o docker-compose.yml https://raw.githubusercontent.com/<you>/<your-repo>/main/docker-compose.yml(Or just copy the docker-compose.yml from this repo into the folder.)
3. Edit docker-compose.yml:
- Fill in
VPN_SERVICE_PROVIDER,WIREGUARD_PRIVATE_KEY,WIREGUARD_ADDRESSES,WIREGUARD_PRESHARED_KEY(if your provider uses one),SERVER_REGIONS/SERVER_CITIESwith your own values. - Set
TZto your timezone. - Set
PUID/PGIDto match your host user (id -u/id -gon Linux). - Change the WebUI port from
8080if you want - but if you do, update all four places it appears:WEBUI_PORT, theports:mapping,FIREWALL_INPUT_PORTS, andQBT_URLinside the watchdog script. This is the single most common misconfiguration people hit - the port must match everywhere.
4. Bring it up:
docker compose up -d5. Check gluetun is actually connected:
docker logs gluetun | grep "Public IP"You should see a line like Public IP address is <vpn ip> (<region>). If you don't see this within a minute or two, your VPN credentials or region/city settings are wrong - check docker logs gluetun for the actual error.
6. Open the WebUI at http://<your-host-ip>:8080 (default qBittorrent login is admin / a randomly generated password printed in docker logs qbittorrent on first run - check there and change it immediately).
Docker can say Gluetun is healthy even when the VPN tunnel is actually dead. So if the watchdog only checks Gluetun's Docker health status, it may leave qBittorrent running while the VPN is broken.
So every check the watchdog does is two signals, both required:
docker inspect gluetun'sHealth.Statussayshealthy.- An independent test actually run inside gluetun's network namespace (
docker exec gluetun wget ... https://www.cloudflare.com/cdn-cgi/trace) succeeds.
Only if both pass does the watchdog consider the VPN genuinely up. Step 1 alone is not trusted for anything.
- Both checks pass, qbittorrent not running -> starts qbittorrent, waits 5s for the WebUI to come up, then (if it's been 12+ hours since the last tracker push) refreshes qBittorrent's tracker list.
- Both checks pass, qbittorrent already running, 12+ hours since last tracker refresh -> refreshes trackers.
- Either check fails -> stops qbittorrent immediately (graceful, 10s grace period - it flushes resume data properly, no corrupted torrents).
- Both checks have failed continuously for 5 minutes -> gluetun isn't just slow to reconnect on its own anymore, something's actually stuck. As a last resort, the watchdog runs
docker restart gluetun- then goes right back to waiting for both checks to pass again before touching qbittorrent. There's no fixed sleep-then-restart-qbittorrent sequencing anywhere in this script; qbittorrent only ever starts once both checks have just confirmed things are actually working, no matter how long that takes. (An earlier version of this used a timed restart sequence and it caused exactly the failure you'd expect: qbittorrent starting into a network namespace gluetun hadn't finished rebuilding yet, ending up "running" but with no working route out.)
The tracker refresh happens automatically whenever both checks are confirmed passing, plus a fresh push every time qBittorrent comes back up after a drop. It pulls from ngosang/trackerslist (trackers_best.txt) and pushes via qBittorrent's setPreferences API. Swap TRACKERS_URL in the script for any other tracker list URL if you prefer, TEST_URL for a different connectivity-check target, or change INJECT_INTERVAL/RESTART_THRESHOLD (both in seconds) to adjust the cadences.
Gluetun's firewall blocks all inbound connections by default - including from your own LAN - unless you explicitly open a port with FIREWALL_INPUT_PORTS. Without this, qBittorrent's WebUI is reachable from other containers on the same Docker network (which is how a reverse proxy on that network would still work) but not from a direct LAN client hitting http://<host-ip>:8080.
To start/stop qBittorrent based on gluetun's state, the watchdog needs to talk to the Docker API - there's no way around this for container-to-container lifecycle control. Mounting /var/run/docker.sock gives it that access, but be aware: anything with access to the Docker socket has root-equivalent control over the entire host, not just this stack. This is the same level of trust you're already extending to Portainer's own agent, if you use one. Don't add untrusted images to this stack.
- WireGuard silently stops passing traffic (it's a silent protocol - no explicit "disconnected" event).
- qBittorrent has already had zero leak exposure from the first dropped packet - it has no other network path, period, regardless of anything below.
- On its very next poll (within 10s),
torrent-vpn-watchdogruns its own connectivity test through gluetun's network namespace. This does not wait for gluetun's own internal detection or Docker healthcheck to catch up - it's an independent, direct test, so it catches the failure on the watchdog's own ~10-15s cycle even in the case where gluetun's status is still (wrongly) reporting healthy. - The watchdog stops qBittorrent immediately (graceful, resume data flushed properly).
- Meanwhile, gluetun may or may not be fixing itself in the background - its own internal reconnect logic sometimes recovers on its own, and sometimes it doesn't and just sits there. The watchdog doesn't wait to find out which; qBittorrent stays stopped either way until the independent check actually passes.
- There's a real but small window - roughly the ~10-15s between the actual drop and the watchdog's next check - where a freshly-submitted *arr job could get accepted by qBittorrent's WebUI before it's stopped. It stalls at 0 peers for that window, then gets cut off. Not a leak, just a short stall. Sonarr/Radarr handle a subsequently-unreachable download client gracefully and retry.
- If gluetun still hasn't recovered after 5 minutes of continuous failure, the watchdog force-restarts gluetun's container as a last resort, then goes back to waiting for a genuine pass before touching qBittorrent again.
- Once both checks confirm the tunnel is actually working again, the watchdog starts qBittorrent - a fresh process, not a resumed stale one - and pushes a fresh tracker list.
qBittorrent WebUI unreachable, but docker ps shows everything running:
Check FIREWALL_INPUT_PORTS is set and matches your WebUI port. Also confirm nothing else on your network is proxying to the wrong port.
torrent-vpn-watchdog logs are empty:
That's expected - it doesn't log routine start/stop/inject actions by design. Check docker inspect -f '{{.State.Health.Status}}' gluetun directly to see what it's seeing, or watch docker events --filter container=qbittorrent while it's running.
Gluetun never reports healthy:
docker logs gluetun - nearly always a wrong private key, wrong server region/city for your account, or the WireGuard endpoint port being blocked by your ISP (try setting WIREGUARD_ENDPOINT_PORT=443).
Tracker injection isn't working:
Exec into the watchdog (docker exec -it torrent-vpn-watchdog sh) and manually run the curl calls from the script to see the actual error - most commonly a wrong QBT_URL port, or qBittorrent's WebUI auth blocking unauthenticated API calls (check Options -> WebUI -> Authentication - "Bypass authentication for clients on localhost/whitelisted subnets" needs to cover the Docker network the watchdog talks over, or you'll need to add API credentials to the script).
- gluetun - VPN client container with a built-in health/reconnect attempt of its own - just not one reliable enough for this stack to trust on its own, which is why the watchdog double-checks it.
- ngosang/trackerslist - the tracker list this stack refreshes from by default.
- reddit post - the reddit discussion that inspired me to move away from my rutorrent + OpenVPN setup over 3 years ago.
MIT - do whatever you want with this, no warranty.