Docker Compose stack for Radarr, Sonarr, Bazarr, SABnzbd, Prowlarr, Profilarr, Configarr, Jellyseerr (Seerr), and Jellyfin. Download clients and indexers use Gluetun with Mullvad WireGuard; Jellyfin stays on the default bridge with ports exposed for LAN streaming.
- Docker with Compose v2.
- CONFIG_ROOT and DATA_ROOT in
.env(defaults'./config'and'./data'). Relative values resolve from the compose project directory when you use a project-local.env; prefer absolute paths (e.g.CONFIG_ROOT='/home/you/arr-new/config') or always run./bin/stack up -dso bind mounts never point at an empty directory. - PUID / PGID: match
id -uandid -gon the host unless you use PUID=0 / PGID=0 (root in containers, weaker isolation). Compose fix-bind-perms chowns CONFIG_ROOT and DATA_ROOT to that pair. Faster manual fix:./bin/fix-bindmount-permissions.sh --data-top-only.
-
Copy and edit the environment file:
cp .env.example .env
-
Set Mullvad WireGuard in
.env:WIREGUARD_PRIVATE_KEY,WIREGUARD_ADDRESSES. Optional:SERVER_COUNTRIES,SERVER_CITIES,VPN_TYPE,VPN_SERVICE_PROVIDER(defaults to mullvad). -
Set CONFIG_ROOT and DATA_ROOT to absolute host paths. Set TZ, PUID, and PGID.
-
If Gluetun cannot reach the LAN or other services, set FIREWALL_OUTBOUND_SUBNETS (include your LAN CIDR). If indexers fail DNS through the VPN, set DNS_UNBLOCK_HOSTNAMES (comma-separated hostnames).
-
Optional: prepare library folders — see Storage layout.
-
Start the stack:
./bin/stack up -d
VPN-routed apps exit through Gluetun. Published ports are on the gluetun container:
| Port | Service |
|---|---|
| 8080 | SABnzbd |
| 7878 | Radarr |
| 8989 | Sonarr |
| 6767 | Bazarr |
| 9696 | Prowlarr |
| 6868 | Profilarr |
| 5055 | Jellyseerr |
Gluetun also exposes 9090/tcp; nothing in this compose file uses it by default.
Jellyfin uses 8096/tcp (HTTP) and 7359/udp (discovery) on its own container.
Configarr has no web UI/port in this stack. It runs as an on-demand worker container.
The reverse-proxy service runs nginx with network_mode: host, listens on 8888, and routes by Host to localhost on the same ports Gluetun and Jellyfin already publish (with X-Forwarded-* headers for Seerr and others).
-
Add static hostnames (replace the IP with this machine’s LAN address):
192.168.178.x seerr.arr.local radarr.arr.local sonarr.arr.local bazarr.arr.local 192.168.178.x sabnzbd.arr.local prowlarr.arr.local profilarr.arr.local jellyfin.arr.local -
Open apps at
http://<name>.arr.local:8888(example:http://seerr.arr.local:8888). -
In Seerr → Settings → General → Application URL, use the same URL you type in the browser, including
:8888. -
Config lives in
config/reverse-proxy/conf.d/arr-stack.conf. For port 80, changelisten 8888tolisten 80everywhere in that file (avoid conflicts with another web server).
Direct access on :5055, :7878, and so on still works without the proxy.
FIREWALL_OUTBOUND_SUBNETS defaults to Docker bridge ranges (172.17.0.0/16, 172.18.0.0/16). Add your LAN (e.g. 192.168.178.0/24) if Jellyseerr must reach Jellyfin on the host or another machine; otherwise Jellyfin login during setup may fail with 404 on POST /api/v1/auth/jellyfin. GET /api/v1/auth/me returning 403 when logged out is normal. DNS_UNBLOCK_HOSTNAMES is empty by default; set it if indexers need DNS outside the VPN tunnel.
host.docker.internal is available inside Gluetun to reach the host when needed.
Use one host share (or directory) for DATA_ROOT — in this example it is called data. Folders are all lowercase so paths match on Linux (case-sensitive). This matches the spirit of the TRaSH Docker folder guide: downloads live under torrents and usenet; libraries (what you give Jellyfin, Plex, or Emby) live under media.
data
├── torrents
│ ├── books
│ ├── movies
│ ├── music
│ └── tv
├── usenet
│ ├── incomplete
│ └── complete
│ ├── books
│ ├── movies
│ ├── music
│ └── tv
└── media
├── books
├── movies
├── music
└── tv
In containers the same tree appears under /data (compose bind-mounts DATA_ROOT → /data for *Arr and SABnzbd). Jellyfin also mounts DATA_ROOT → /data, so paths like /data/media/movies match everywhere.
./bin/bootstrap-media-paths.sh creates the torrents, usenet, and media subtree under DATA_ROOT. If Radarr / Sonarr databases exist but RootFolders is empty, it seeds /data/media/movies (Radarr) and /data/media/tv (Sonarr).
bin/create-single-data-volume.sh creates the same tree on a fresh /data volume (see script header for warnings).
| Purpose | Path inside containers |
|---|---|
| Radarr movie root | /data/media/movies |
| Sonarr TV root | /data/media/tv |
| Jellyfin (main libraries) | /data/media/movies, /data/media/tv, and optionally /data/media/{books,music} |
Root folder in Radarr/Sonarr means the library tree only (**/data/media/...**). Do not add /data/usenet/complete/movies (or any .../usenet/...) as a root folder—that path is for SABnzbd output and *Arr imports. If Radarr says “Download client SABnzbd places downloads in the root folder …/usenet/complete/movies”, you have that Usenet path (or a parent of it) registered as a Root Folder. Remove it under Settings → Media Management → Root Folders and keep only /data/media/movies.
Keep completed Usenet output under /data/usenet/complete/... and let *Arr import (hardlink) into /data/media/.... Enable Use Hardlinks instead of Copy in Radarr / Sonarr → Settings → Media Management so imports work when usenet and media are on the same filesystem.
| Setting | Path |
|---|---|
| Temporary Download Folder | /data/usenet/incomplete |
| Completed Download Folder | /data/usenet/complete (the parent of movies / tv / … — not .../complete/movies as the global default) |
Categories (optional per-category folder; match names you use in *Arr):
| Category | Path |
|---|---|
| movies | /data/usenet/complete/movies |
| tv | /data/usenet/complete/tv |
| music | /data/usenet/complete/music |
| books | /data/usenet/complete/books |
Use books / music only if you configure download clients and *Arr apps for them; otherwise leave Default or omit those categories.
Radarr’s Download Client category movies only tells SABnzbd which SABnzbd category to use; completed files still land under /data/usenet/complete/movies via the table above, while the movie library remains /data/media/movies—two different paths, which is what *Arr expects.
If you add qBittorrent or another client later, point its save paths at /data/torrents/{movies,tv,...} so seeds and incomplete data stay beside usenet and media under one /data tree (same rules for hardlinks into /data/media).
Apps behind Gluetun share one network namespace; use http://127.0.0.1:<port> when one container talks to another.
-
Indexers → Add indexer → protocol Usenet. Search for Generic Newznab (not a branded preset). Set the indexer URL and API key, then Test and Save. Example (SceneNZBs): URL
https://scenenzbs.com, API key from your account on scenenzbs.com. -
Settings → Apps → add Radarr and Sonarr: base URL
http://127.0.0.1:7878andhttp://127.0.0.1:8989, with each app’s API key from Settings → General → Security. Test, Save, then sync indexers when prompted.
In each app: Settings → Download Clients → add SABnzbd:
- Host:
127.0.0.1 - Port:
8080 - API key: from SABnzbd Config → General (or security section, depending on version)
- Category: e.g.
movies(Radarr) andtv(Sonarr), matching SABnzbd category names
Test, then Save.
Profilarr manages quality profiles (optional); it does not replace SABnzbd as a download client.
configarr uses config/configarr for config.yml and secrets.yml, and caches repos in config/configarr/repos.
Run it on demand:
./bin/stack run --rm configarr| Script | Purpose |
|---|---|
bin/stack |
Runs docker compose with the repo as cwd. Example: ./bin/stack up -d. |
bin/bootstrap-media-paths.sh |
Creates TRaSH-style media, usenet, torrents trees (lowercase books / movies / music / tv); seeds empty Radarr/Sonarr root folders to /data/media/movies and /data/media/tv when RootFolders is empty. |
bin/fix-bindmount-permissions.sh |
Same chown idea as compose fix-bind-perms; --data-top-only avoids a full recursive DATA_ROOT pass. |
bin/create-single-data-volume.sh |
Destructive, machine-specific: RAID0 from sda + sdb + sdc, mount at '/data', create torrents / usenet / media tree (same layout as above). Runs docker compose down before disk work. |
bin/assemble-md0.sh |
Reassemble existing Linux RAID on sda + sdb, mount ext4 at '/mnt/md0'. |
bin/setup-mergerfs-pool.sh |
Optional mergerfs pool ('/mnt/md0:/mnt/disk-sdc' → '/mnt/media-pool' by default). |
bin/restore-data-from-external.sh |
Copy data-backup/ from a mounted drive (default '/mnt/external') to '/data'. |
bin/prune-usenet-root-folders.sh |
Removes Radarr / Sonarr Root Folder entries whose path contains usenet (fixes “download client … places downloads in the root folder …/usenet/…”). Requires running radarr / sonarr containers. |
See TROUBLESHOOTING.md (for example indexer HTTP 429 / rate limits, or Radarr “download to root folder” / usenet paths).