--- name: self-hosted-torrent-client description: |- Deploy and troubleshoot a Dockerized torrent client (qBittorrent) behind a VPN (Gluetun) with private tracker support, including dynamic IP registration via Mousehole for trackers like MyAnonaMouse (MAM). version: 1.0.0 platforms: [linux] metadata: hermes: tags: [torrent, seedbox, vpn, gluetun, qbittorrent, private-tracker, mam] category: self-hosting --- # Self-Hosted Torrent Client (qBittorrent + Gluetun + Private Trackers) Deploy and maintain a Dockerized qBittorrent behind a VPN tunnel (Gluetun) with support for private trackers that require IP registration (MAM, etc.). ## Architecture ``` qBittorrent ──┤ │ │ network_mode: │ Mousehole ───┤ service:gluetun │── tun0 (VPN) ── Internet │ │ Gluetun ──────┤ VPN client │── enp2s0 (host network) ``` All three containers share Gluetun's network namespace. Only Gluetun declares port mappings — qBittorrent and Mousehole inherit them. ## Quickstart ### 1. Docker Compose Use a single compose file with three services: ```yaml 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 - "5010:5010" # Mousehole WebUI (use 0.0.0.0:5010 if accessing from LAN) environment: VPN_SERVICE_PROVIDER: "private internet access" VPN_TYPE: "openvpn" OPENVPN_PROTOCOL: "tcp" OPENVPN_USER: "" OPENVPN_PASSWORD: "" SERVER_HOSTNAMES: "nl-amsterdam.privacy.network" VPN_PORT_FORWARDING: "on" PORT_FORWARD_ONLY: "true" FIREWALL_VPN_INPUT_PORTS: "8080,5010" restart: unless-stopped qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent network_mode: "service:gluetun" depends_on: gluetun: condition: service_healthy volumes: - ./qbittorrent:/config - /path/to/downloads:/downloads environment: - PUID=1000 - PGID=1000 - TZ=America/Chicago - WEBUI_PORT=8080 restart: unless-stopped mousehole: image: tmmrtn/mousehole:latest container_name: mousehole network_mode: "service:gluetun" depends_on: gluetun: condition: service_healthy environment: TZ: America/Chicago MOUSEHOLE_AUTH_PASSWORD: MOUSEHOLE_ALLOWED_HOSTS: localhost,127.0.0.1,[::1],192.168.50.98 volumes: - mousehole:/var/lib/mousehole restart: unless-stopped volumes: mousehole: ``` ### 2. Set up qBittorrent - Access WebUI at `http://:8080` - Go to Settings → Connection - Set **Listening Port** to match Gluetun's forwarded port - Disable UPnP/NAT-PMP - The port is stored in `Session\Port` in `qBittorrent.conf` - Verify the forwarded port from Gluetun: `docker exec gluetun cat /tmp/gluetun/forwarded_port` ### 3. Set up Mousehole (for private trackers needing dynamic IP) - Access Mousehole WebUI at `http://:5010` - Log in with the `MOUSEHOLE_AUTH_PASSWORD` - On MAM, go to **Preferences → Security** and create a session: - IP: Use the IP Mousehole shows you - IP vs ASN locked: **ASN** - Allow Session to set Dynamic Seedbox: **Yes** - Label: `mousehole` - Copy the cookie MAM gives you - Paste it into Mousehole and click **Set** ## Common Private Tracker Issues | Symptom | Likely Cause | Fix | |---|---|---| | "Unrecognized host/PassKey" | Browser IP ≠ VPN IP (private tracker mismatch) | Set up Mousehole with MAM Dynamic Seedbox API | | "Non-Whitelisted client or version" | Client not on tracker's allowed list | Check tracker's allowed clients page; may need to downgrade/pin a version | | Stalled torrent, no peers | Tracker not being contacted or port not reachable | Verify QBT port matches forwarded port, check tracker reachability: `curl -s -o /dev/null -w "%{http_code}" ""` | | Port mismatch | Gluetun forwarded port ≠ QBT listening port | After Gluetun restart, check `docker exec gluetun cat /tmp/gluetun/forwarded_port` and update QBT config | ## Pitfalls & Gotchas ### qBittorrent WebUI: IPv6-Only Binding on linuxserver.io Images **Symptom:** qBittorrent logs "WebUI: Now listening on IP: *, port: 8080" but the WebUI is unreachable from the Docker host via IPv4. Connection gets "Connection refused" on the container's Docker IP, even though qBittorrent is running and healthy. **Root cause:** On linuxserver.io qBittorrent images (v5.2.x), the start script changes `WebUI\Address=*` to `WebUI\Address=localhost` before launch. qBittorrent 5.2.x then binds the WebUI **only to IPv6** (`[::]:8080`). The Docker proxy connects via IPv4 (the container's Docker IP), which fails because nothing is listening on IPv4:8080. **Diagnosis:** ``` docker exec qbittorrent sh -c 'cat /proc/net/tcp /proc/net/tcp6' | grep -i "1F90" ``` - `/proc/net/tcp` (IPv4) — port 8080 (0x1F90) ABSENT - `/proc/net/tcp6` (IPv6) — port 8080 PRESENT on `[::]:1F90` **Fix:** Set `WebUI\Address=0.0.0.0` in qBittorrent.conf (instead of `*`). The start script's localhost override only triggers on empty or `*` values. Then restart qBittorrent. ### Port Binding - `127.0.0.1:5010:5010` binds to localhost ONLY — not accessible from LAN or other containers - Use `0.0.0.0:5010:5010` or just `5010:5010` for LAN access - Default `docker compose restart` does NOT pick up port binding changes — use `docker compose up -d --force-recreate ` instead ### Mousehole Host Header - Default `MOUSEHOLE_ALLOWED_HOSTS` only allows `localhost`, `127.0.0.1`, `[::1]` - Add your LAN IP or hostname when accessing from another machine ### Gluetun Recreates - When Gluetun is recreated, qBittorrent may auto-detach if using `network_mode: service:gluetun` - Docker Compose handles this correctly via container name, not container ID - The PIA VPN IP WILL change on restart — Mousehole handles re-registration ### Gluetun VPN IP Changes - PIA and other VPN providers assign different IPs on reconnect - This perfectly normal — Mousehole re-registers the new IP with the tracker - Check current VPN IP: `docker exec gluetun cat /tmp/gluetun/ip` ### Stale Containers - Old, unhealthy gluetun instances (e.g., `vigorous_lewin`) can sit around after config changes - Check for them with `docker ps -a` and clean up: `docker rm ` ### Port Forwarding Verification 1. Check forwarded port: `docker exec gluetun cat /tmp/gluetun/forwarded_port` 2. Check what port QBT is listening on from its logs or config 3. Test port is open on VPN interface: `docker exec qbittorrent python3 -c "import socket; s=socket.socket(); s.settimeout(3); print(s.connect_ex(('10.26.x.x', 36590)))"` (use actual tun0 IP from QBT logs) ### Tracker Connectivity Test - From inside the container: `docker exec qbittorrent curl -s -o /dev/null -w "%{http_code} (%{time_total}s)" --connect-timeout 15 ""` - A 404 on the announce URL is expected (missing required params) — means the tracker is reachable - A 200 with "failure reason" means tracker is rejecting the announce specifically — check the reason text ## Verification After setup, confirm: 1. Gluetun is healthy: `docker ps --filter name=gluetun --format '{{.Status}}'` 2. Port forwarding is active: `docker exec gluetun cat /tmp/gluetun/forwarded_port` 3. qBittorrent is using the forwarded port from its config 4. Mousehole shows OK status after cookie is set 5. Add a public tracker torrent to qBittorrent to verify general connectivity 6. Add the private tracker torrent and check tracker status via MAM's tracker detail page