Files
2026-07-12 10:17:17 -04:00

195 lines
7.7 KiB
Markdown

---
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: "<your-username>"
OPENVPN_PASSWORD: "<your-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: <strong-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://<host-ip>: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://<host-ip>: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}" "<tracker-announce-url>"` |
| 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 <service>` 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 <container>`
### 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 "<tracker-url>"`
- 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