195 lines
7.7 KiB
Markdown
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
|