| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Deploy / deploy (push) Successful in 44s
Reviewed-on: #7 |
||
| .forgejo/workflows | ||
| .env.example | ||
| .gitignore | ||
| docker-compose.yml | ||
| README.md | ||
| renovate.json | ||
watchlist
Automated media stack for Jellyfin, deployed separately from Jellyfin itself.
Services in this project:
gluetun(ProtonVPN WireGuard gateway)qbittorrent(torrent download client)sabnzbd(usenet download client)prowlarr(indexer manager)sonarr(TV)radarr(movies)seerr(request UI for users)
Architecture
- VPN-routed services (through
gluetun):qbittorrent,sabnzbd,sonarr,radarr,prowlarr - Non-VPN services:
seerr - Shared media/download storage: host
${MEDIA_ROOT}mounted as/datainqbittorrent,sabnzbd,sonarr,radarr.prowlarrhas no media mount — it only manages indexers. - Jellyfin stays separate, only reads the library directories
This keeps playback stable even if VPN or automation services fail.
Prerequisites
- Docker + Docker Compose installed
- Existing Traefik network named
traefik(or create it) - ProtonVPN WireGuard config values:
WIREGUARD_PRIVATE_KEYWIREGUARD_ADDRESSES
Directory Layout
Default MEDIA_ROOT is /mnt/media (NFSv3, volume2). The older
/mnt/nas-direct (CIFS/SMB, volume1) is being phased out — see
NAS-SMB-TO-NFS-MIGRATION.md on the host.
Expected structure:
media/library/moviesmedia/library/tvmedia/rips/moviesmedia/rips/tvdownloads/torrents/incompletedownloads/torrents/completedownloads/usenet/incompletedownloads/usenet/complete
Create it:
mkdir -p \
/mnt/media/media/library/movies \
/mnt/media/media/library/tv \
/mnt/media/media/rips/movies \
/mnt/media/media/rips/tv \
/mnt/media/downloads/torrents/incomplete \
/mnt/media/downloads/torrents/complete \
/mnt/media/downloads/usenet/incomplete \
/mnt/media/downloads/usenet/complete
Configuration
- Copy env template:
cp .env.example .env
- Fill required values in
.env:
WIREGUARD_PRIVATE_KEYWIREGUARD_ADDRESSES
- Optional VPN filtering:
SERVER_COUNTRIES=United StatesSERVER_CITIES=(blank is usually best initially)
- Optional LAN allow-list from VPN containers:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
Use only if VPN-contained apps must call LAN IPs.
Deploy
Pushing to main triggers .forgejo/workflows/deploy.yml, which writes .env
from Forgejo secrets and redeploys. It runs docker compose down plus
docker rm -f on all seven containers, so every deploy briefly takes the whole
stack offline and forces a ProtonVPN reconnect. Deploy when nobody is mid-download.
Manual deploy:
cd /home/bobparsons/Development/watchlist
docker network inspect traefik >/dev/null 2>&1 || docker network create traefik
docker compose -p watchlist pull
docker compose -p watchlist up -d --remove-orphans
docker compose -p watchlist ps
Service URLs
- Seerr:
https://${SEERR_DOMAIN} - Prowlarr:
http://<host-ip>:9696 - Sonarr:
http://<host-ip>:8989 - Radarr:
http://<host-ip>:7878 - SABnzbd:
http://<host-ip>:8085 - qBittorrent:
http://<host-ip>:8080
Authentication
Sonarr and Radarr run with <AuthenticationMethod>External</AuthenticationMethod>
(built-in login disabled) as of 2026-09-13. Neither has a Traefik route; both are
reachable only on the LAN through the ports gluetun publishes.
This setting lives in config/<app>/config.xml, which is gitignored and
bind-mounted, so it persists across redeploys and is not represented in this repo.
To re-enable logins, set the value back to Forms and set a username/password
under Settings → General → Security. The pre-change files are kept alongside as
config.xml.pre-authfix.*.
Prowlarr was switched to External the same way on 2026-09-20 (its Forms password
had been lost). Same file, same revert path; backup kept as config.xml.bak-*.
Note the account username is deadstylebp, not admin.
First-Time App Wiring
1) Download clients in Sonarr/Radarr
Add SABnzbd:
- Host:
localhost - Port:
8085 - Categories:
- Sonarr:
tv - Radarr:
movies
- Sonarr:
(Optional) Add qBittorrent:
- Host:
localhost - Port:
8080 - Matching categories if you use torrents
2) Root folders
Use container paths:
- Sonarr:
/data/media/library/tv - Radarr:
/data/media/library/movies
Enable hardlinks where available in Media Management.
3) Prowlarr
- Add indexers you have access to (e.g. NZBGeek)
- Add Applications (prowlarr shares gluetun's network namespace, so these are
loopback, not container names):
- Prowlarr Server:
http://localhost:9696 - Sonarr:
http://localhost:8989 - Radarr:
http://localhost:7878
- Prowlarr Server:
- Use each app's API key
4) Seerr
- Connect to Jellyfin
- Add Sonarr and Radarr in Seerr using:
- Sonarr:
http://gluetun:8989 - Radarr:
http://gluetun:7878
- Sonarr:
VPN Port Forwarding
Deliberately off. Gluetun supports ProtonVPN port forwarding, and it was briefly enabled on 2026-09-20 and then reverted.
Why it is off:
- Only public trackers are in use, so there is no ratio to maintain and nothing that requires being dialable from the swarm.
- Well-seeded torrents do not need it. Measured without a forwarded port: ~28 GB pulled at 20-25 MB/s from a swarm with ~950 seeders.
- Enabling it costs something. Gluetun implicitly turns on "port forwarding only servers", which narrows server selection.
It also does not work with the current WireGuard key. Proton only answers NAT-PMP for configs generated with port forwarding enabled -- it is a property of the key, not a client-side request. With the current key gluetun logs, harmlessly and on every start:
[port forwarding] ... read udp 10.2.0.2:45081->10.2.0.1:5351:
recvfrom: connection refused
To revisit (worth it only if a private tracker with ratio requirements is added, or if poorly-seeded torrents become common):
- Proton dashboard -> Downloads -> WireGuard configuration, pick a P2P server, toggle NAT-PMP (Port Forwarding) on, generate a new config.
- Update the
WIREGUARD_PRIVATE_KEYsecret (andWIREGUARD_ADDRESSESif it changed) and redeploy. - Add back to
gluetun's environment:VPN_PORT_FORWARDING=onandVPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port - Read the assigned port from
config/gluetun/forwarded_portand set it as qBittorrent's listening port. Proton rotates this port, so either update it manually or drive it fromVPN_PORT_FORWARDING_UP_COMMAND, which needs qBittorrent's WebUI credentials.
Torrent Seeding and Cleanup
Set in qBittorrent on 2026-09-20 (config/qbittorrent/.../qBittorrent.conf,
gitignored like the other app configs):
Session\GlobalMaxRatio=2Session\GlobalMaxSeedingMinutes=14400(10 days)Session\ShareLimitAction=Stop(pre-existing)
Sonarr and Radarr have removeCompletedDownloads=True, but that only fires once
qBittorrent considers seeding finished. With no limits set, that never happened
and downloads/torrents grew without bound. Without port forwarding the 2.0 ratio
is rarely reached, so in practice the 10-day limit is what retires torrents.
Both apps also recycle rather than hard-delete: recycleBin=/data/.recycle
(host /mnt/media/.recycle, uid 1000), 7-day retention.
Jellyfin Integration
In Jellyfin, mounts and libraries should remain read-only. Jellyfin currently reads movies/TV from the NFS mount and music from the older SMB mount:
/mnt/media/media/library/movies:/media/library/movies:ro/mnt/media/media/library/tv:/media/library/tv:ro/mnt/nas-direct/media/music:/media/music:ro(not yet migrated)- Libraries:
/media/library/movies/media/library/tv
Keep manual rips under /media/rips/* as separate staging/input paths.
Operational Checks
Verify VPN routing
curl -s https://api.ipify.org && echo
docker exec media-radarr sh -lc 'curl -s https://api.ipify.org && echo'
Host IP and container IP should differ if VPN routing is active.
Common useful commands
docker compose -p watchlist logs -f gluetun
docker compose -p watchlist logs -f radarr
docker compose -p watchlist logs -f sonarr
docker compose -p watchlist ps
Common Issues
-
Download client unavailable- Verify SAB/qB is added with
localhostinside Arr app.
- Verify SAB/qB is added with
-
No indexers available- Check Prowlarr indexer test results.
- Re-sync app indexers from Prowlarr.
-
The /app/config volume mount was not configured properlyin Seerr- Ensure named volume
seerr-configis mounted.
- Ensure named volume
-
Gluetun unhealthy
- Start with
SERVER_COUNTRIES=United Statesand blankSERVER_CITIES. - Confirm WireGuard values are correct.
- Start with
Updating
cd /home/bobparsons/Development/watchlist
docker compose -p watchlist pull
docker compose -p watchlist up -d --remove-orphans
docker image prune -f