---
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
