gluetun stale-firewall trap: qBit stuck at metaDL (media-server CT 102)¶
URL: https://mkdocs.justinsforge.com/memory/general/reference_gluetun_qbit_portforward_bug/
Stack: gluetun (WireGuard VPN, kernelspace) + qbittorrent (network_mode: service:gluetun, shares gluetun netns) on media-server CT 102, compose at /opt/stacks/media. qBit WebUI :8080 published through gluetun. VPN exit is US-Texas-Dallas (159.26.100.51), WireGuard endpoint 95.173.217.217:51820, static forwarded port 56695 (same port re-issued every reconnect, so an unchanged port does NOT prove a restart happened).
Symptom¶
Every torrent frozen at metaDL, 0/0 seeds, 0 B/s; transfer/info shows connection_status: firewalled. Existing 100%-complete torrents also stalledUP with 0 peers. Newly-added Radarr grabs never leave metadata.
Diagnosis path (all these can look FINE and still be broken)¶
- Download path OK:
/mnt/storage/downloadsexists, array has TBs free. (qBit's ownfree_space_on_diskmay report 0 here even with 4.3 TB free perdf— cosmetic qBit quirk, NOT the blocker.) - VPN tunnel OK:
docker exec gluetun wget -qO- https://api.ipify.orgreturns the VPN IP; container healthhealthy; DNS (getent hosts tracker.opentrackr.org) resolves. - qBit listen port == gluetun forwarded port (both 56695). Alignment is fine.
- THE TELL:
GET /api/v2/torrents/trackers?hash=<h>shows UDP trackers =Operation not permitted(EPERM = gluetun firewall dropping outbound UDP), HTTP trackerstimed out, DHT/PeX/LSD 0 nodes. TCP/443 still works but UDP tracker+DHT traffic is rejected → no peer discovery → metaDL forever. - Root cause: gluetun long uptime (was 18 days); tunnel still passes TCP but firewall/routing state drifted so outbound UDP gets EPERM. A genuine restart rebuilds the firewall + tunnel and traffic flows instantly (131 MB/s aggregate within ~75s,
connection_status: connected).
FIX¶
ssh media-server 'docker restart gluetun; sleep 30; for c in qbittorrent prowlarr sonarr radarr; do docker restart $c; done'
CRITICAL netns lesson (2026-07-08): qbittorrent AND prowlarr/sonarr/radarr all run network_mode: service:gluetun (their published ports 8080/9696/8989/7878 all appear on the gluetun container). When gluetun restarts, EVERY dependent is orphaned from the old network namespace and loses outbound connectivity until IT is also restarted. Symptom if you forget one: qBit downloads fine (you restarted it) but Prowlarr/Sonarr searches return 0 candidates and Prowlarr indexer/testall fails ALL indexers with Resource temporarily unavailable (host:443) (even private MyAnonaMouse). Sonarr health shows "All search-capable indexers temporarily unavailable" / "failures for more than 6 hours". Restarting prowlarr+sonarr+radarr restores them; then in Sonarr run POST /api/v3/indexer/testall (and Prowlarr /api/v1/indexer/testall) to force-clear the escalating failure-backoff (a container restart alone does NOT reset the backoff timer in the DB, so searches keep returning 0 until a successful test). Original single-restart form (qbittorrent only) below is INSUFFICIENT — it fixes downloads but silently leaves the indexers dead. gluetun's port-forward push to qBit may log Connection refused if qBit is still restarting — harmless, qBit binds its saved port 56695 on boot anyway. Verify: docker inspect --format '{{.State.StartedAt}}' gluetun shows a fresh timestamp, and qBit transfer/info flips to connected.
GOTCHA: docker compose restart here was a silent no-op (FIXED 2026-07-08)¶
cd /opt/stacks/media && docker compose restart gluetun used to do nothing (StartedAt unchanged, no fresh logs → looked like it "worked"). Root cause was NOT the ${i} warning (that was a red herring — a warning never blocks a restart). Real cause: Compose project-name mismatch. The running stack was deployed under project name media-server (see docker inspect <ctr> --format '{{index .Config.Labels "com.docker.compose.project"}}'), but the folder is /opt/stacks/media, so bare docker compose defaulted the project to the folder basename media — an empty phantom project — and every command no-op'd the real containers. (Folder was likely renamed media-server→media at some point, orphaning the CLI.)
Both fixed in the compose file 2026-07-08: (1) added top-level name: media-server so docker compose in this dir always targets the real stack; (2) escaped the gluetun VPN_PORT_FORWARDING_UP_COMMAND retry loop $i→$$i to silence the interpolation warning. Verified: docker compose ps now lists all 13 services and docker compose restart <svc> actually cycles containers. Backup at docker-compose.yml.bak.pre-i-fix-20260708. So the multi-container restart fix above now works via docker compose too; plain docker restart <name> remains a fine fallback. Always confirm any restart with docker inspect ... StartedAt.
Related: [[reference-overseerr-seerr-request-pipeline]] (Radarr/qBit download path), [[reference-media-server-stack]], [[reference-qbittorrent-save-path]].
Distinct older gotcha: UP_COMMAND silent-fail on setPreferences (found 2026-05-19)¶
Separate from the firewall-drift issue above: gluetun's VPN_PORT_FORWARDING_UP_COMMAND POSTs the forwarded port to qBit's setPreferences API, but the POST body was missing Content-Type: application/x-www-form-urlencoded, so qBit returned HTTP 200 OK and silently ignored the payload. Result: every port-forward refresh, qBit drifted back to its stale listen port, went firewalled, DHT dropped to 0 nodes, UDP trackers failed with Operation not permitted. Gluetun's control API /v1/openvpn/portforwarded also returned 401 (auth required in that gluetun rev) so health checks couldn't query port-forward state directly.
Manual workaround at the time: POST to qBit setPreferences with the correct Content-Type and json={"listen_port": <forwarded_port>}, immediately restores connected + DHT. Permanent fix (add the header to the UP_COMMAND) was NOT confirmed patched as of 2026-05-19; if qBit drifts back to a stale port again after a port-forward refresh (as opposed to the long-uptime firewall-EPERM symptom above), check the UP_COMMAND header first before assuming it's the netns/firewall-drift issue.
[Claude Code]