Skip to content

Reference media server stack

Two compose stacks live on CT 102 (media-server):

  1. /opt/stacks/media/docker-compose.yml, the 13-container arr stack: gluetun, qbittorrent, sonarr, radarr, prowlarr, flaresolverr, overseerr, bazarr, recyclarr, cleanuparr, audiobookshelf, calibre-web, shelfarr
  2. /mnt/storage/appdata/justinkrystal-media/docker-compose.yml, the 3-container justinandkrystal site stack

CRITICAL QUIRK 1 — compose project name: arr stack project is media-server, NOT media (the dirname). Always pass -p media-server to compose commands or they target the wrong project and report ghost orphans.

CRITICAL QUIRK 2 — gluetun netns recreation: every service in the arr stack uses network_mode: container:gluetun. If gluetun is recreated (e.g. you change its ports: block), every dependent container loses its network namespace and must be docker restarted. New containers created against the old gluetun ID will fail to start with "joining network namespace of container: No such container" and must be compose rm -sf then compose up -d again.

Gluetun secrets: /opt/stacks/media/.env (chmod 600), externalized 2026-04-27.

Service config pointers

Service Port API key location Notes
qbittorrent 8080 (web UI password) port-forward auto-pushed by gluetun
sonarr 8989 docker exec sonarr cat /config/config.xml <ApiKey>
radarr 7878 same pattern in radarr container
prowlarr 9696 same pattern in prowlarr container feeds Sonarr/Radarr via app sync
flaresolverr 8191 n/a tagged on Cloudflare-protected indexers
bazarr 6767 docker exec bazarr grep apikey: /config/config/config.yaml (first match = own key) linked via SignalR feeds, see config.yaml sonarr.apikey/radarr.apikey
recyclarr n/a n/a (reads /config/configs/*.yml) sync: docker exec recyclarr recyclarr sync
cleanuparr 11011 UI-managed first-run wizard sets admin + arr/qbit links
overseerr 5055 UI-managed host-network, NOT behind gluetun
audiobookshelf 13378 UI-managed
calibre-web 8083 UI-managed
shelfarr 5056 (mapped to 80) UI-managed

Prowlarr indexer fleet (as of 2026-05-03)

7 working public indexers: 1337x, The Pirate Bay, Knaben (meta-aggregator), EZTV, YTS, TorrentGalaxyClone, LimeTorrents. Plus TorrentsCSV (degraded). 1337x and TPB tagged with FlareSolverr (id=2 in Prowlarr proxies).

Tried but blocked at gluetun VPN exit IP: kickasstorrents (403 on every mirror), Internet Archive (timeout). Don't waste time re-adding without rotating VPN exit.

Recyclarr / TRaSH

Configs in /opt/stacks/media/recyclarr/configs/: - web-1080p.yml (Sonarr) → quality profile "WEB-1080p" + 37 custom formats - hd-bluray-web.yml (Radarr) → quality profile "HD Bluray + WEB" + 39 custom formats

API keys baked into the YAML (Sonarr/Radarr keys are embedded literally). Re-sync after editing: docker exec recyclarr recyclarr sync. Templates list: docker exec recyclarr recyclarr config list templates.

gluetun ↔ qBittorrent port-forward sync (STALL ROOT CAUSE, seen 2026-05-29)

Symptom: torrents stuck at 0% / stalledDL despite available seeders; Sonarr/Radarr queue shows size 0 / queued. Cause: qBit's Session\Port (listen port) no longer matches gluetun's ProtonVPN forwarded port, so no inbound peers connect.

  • gluetun forwarded port: docker exec gluetun cat /tmp/gluetun/forwarded_port
  • qBit listen port: Session\Port in /opt/stacks/media/qbittorrent/qBittorrent/qBittorrent.conf (or via API).
  • Sync mechanism: gluetun VPN_PORT_FORWARDING_UP_COMMAND POSTs setPreferences {listen_port:{{FORWARDED_PORT}}} to qBit. qBit shares gluetun's netns (network_mode: container:gluetun) and has WebUI\LocalHostAuth=false, so the in-container 127.0.0.1:8080 call bypasses auth.
  • Manual fix (no restart, no creds): docker exec gluetun sh -c 'wget -qO- --post-data="json={\"listen_port\":<PORT>}" http://127.0.0.1:8080/api/v2/app/setPreferences' then confirm via /api/v2/app/preferences.
  • Durable fix (INSTALLED 2026-05-29): qbit-vpn-port-sync.timer on media-server runs /opt/stacks/media/qbit_vpn_port_sync.sh every 2 min (+90s after boot). Idempotent reconcile: reads gluetun forwarded_port, compares to qBit listen_port, writes only on mismatch. Self-heals reboots + VPN reconnects + port rotation, independent of gluetun's flaky up-command. No container recreate needed. Source-of-truth copies in forge at scripts/integrations/media-server/. Logs to journal tag qbit-port-sync (silent when already in sync). Verified self-heal: forced bogus port -> corrected next run.
  • (Legacy note) the RUNNING gluetun still has the old single-shot up-command; compose ~line 22 has a 12x retry-loop version not yet applied. Moot now that the timer reconciles, but recreating gluetun would apply it (WARNING: netns dependency means every arr container must be docker restarted after — see Quirk 2).
  • Read qBit state without WebUI creds: run any /api/v2/... GET via docker exec gluetun wget -qO- http://127.0.0.1:8080/... (localhost-bypass). The qBit WebUI user is justinwieb; the cred stored in Sonarr's download-client config (root) is stale and rejected.

Plex library update

Radarr (id=3) and Sonarr (id=3) each have a "Plex Server" notification (PlexServer implementation) pointed at 192.168.86.73:32400. Triggers on import, upgrade, rename, delete. This tells Plex to scan the relevant library section immediately after Radarr/Sonarr changes files, so new content appears without waiting for Plex's periodic scan. Added 2026-05-26.

Telegram notifications

Sonarr + Radarr each have a built-in Telegram notification connector (id=2 in each /api/v3/notification) named "Forge Telegram (notify-out)", wired to @forge_notify_outbound_bot (token in ~/.forge-secrets/telegram-notify-outbound.env, chat 7110798439). Triggers: grab, download, upgrade, import-complete, manual-interaction-required, health issue/restored.

Cleanuparr

Stores everything in SQLite at /opt/stacks/media/cleanuparr/cleanuparr.db. No plain config file. UI-only setup; can't be bootstrapped via API ("Setup required" error until wizard completes).