Stack: Jellyfin, Sonarr, Radarr, Prowlarr, Seerr, qBittorrent.
Request management is handled by Seerr — the unified successor project that merges Jellyseerr and Overseerr into one actively maintained app supporting Jellyfin, Plex, and Emby.
All images are LinuxServer.io builds, except Seerr which is the
project's own official image — both are multi-arch and run fine on
your Oracle ARM VM. One difference worth knowing: Seerr doesn't use the
PUID/PGID pattern the LinuxServer images do — it always runs as its
built-in node user (UID 1000), so its config folder needs a manual
chown (covered in the folder structure step below).
Note on privacy: this version has no VPN in front of qBittorrent, so
its torrent traffic goes out directly on your VM's own connection/IP.
If you later pick up a WireGuard-capable VPN subscription, it's a small
change to add a wireguard container back and route qBittorrent's
traffic through it — see the note at the end of this guide.
- qbittorrent / jellyfin / sonarr / radarr / prowlarr / seerr each run on two networks: an internal one for talking to each other, and a shared network with your existing Nginx Proxy Manager (NPM) instance so it can reverse-proxy them without you opening ports beyond what's needed.
- Docker + the Docker Compose plugin installed on the VM.
- Your existing NPM instance's Docker network name (see step 6).
- A storage mount with enough room for your media + downloads, ideally a block volume attached to the VM rather than the boot disk.
Keep media and downloads under the same root so Sonarr/Radarr can hardlink files during import instead of copying them:
/mnt/data/ # <- DATA_ROOT in .env
├── media/
│ ├── movies/
│ └── tv/
└── torrents/
├── movies/
└── tv/
Create it:
sudo mkdir -p /mnt/data/media/movies /mnt/data/media/tv \
/mnt/data/torrents/movies /mnt/data/torrents/tv
sudo chown -R $USER:$USER /mnt/dataThen, alongside your docker-compose.yml, create per-service config
folders:
mkdir -p config/{qbittorrent,jellyfin,sonarr,radarr,prowlarr,seerr}Seerr's container always runs as UID 1000 internally (it ignores
PUID/PGID), so its config folder specifically needs to be owned by
1000:1000 regardless of what your own user's ID is:
sudo chown -R 1000:1000 config/seerrcp .env.example .envFill in PUID/PGID (run id $USER), TZ, DATA_ROOT, and
NPM_NETWORK_NAME (covered in step 6).
docker network ls
docker inspect <your-npm-container-name> --format '{{json .NetworkSettings.Networks}}'Put whatever network name that prints into NPM_NETWORK_NAME in .env.
If NPM is running as a standalone container (not compose-managed) on the
default bridge, it's simpler to create a dedicated network and reconnect
NPM to it:
docker network create npm_proxy_network
docker network connect npm_proxy_network <your-npm-container-name>docker compose up -d
docker compose logs -fCheck qBittorrent's logs for its auto-generated first-run WebUI password:
docker compose logs qbittorrent | grep -i password(You'll change this immediately in the WebUI anyway.)
If you're managing this through Coolify instead of the CLI, add it as a
"Docker Compose" resource pointing at this file — Coolify will handle
up/down for you, but you still need the folders and .env created
on the host first.
- Open Prowlarr → Indexers → add your trackers/indexers.
- Settings → Apps → add Sonarr and Radarr using each app's internal
URL (
http://sonarr:8989,http://radarr:7878) and API key (found in each app's Settings → General). Prowlarr will then push indexers to both automatically.
- Log in at
http://<vm-ip>:8080, change the password immediately (Settings → WebUI). - Settings → Downloads → set default save path to
/data/torrents, and enable "Keep incomplete torrents in a separate folder" if you want.
- Settings → Media Management → add root folder:
/data/media/tv(Sonarr) or/data/media/movies(Radarr). - Settings → Download Clients → add qBittorrent, using host
qbittorrent, port8080. - Prowlarr will populate indexers automatically once connected above.
- First-run wizard → add libraries pointing at
/data/media/moviesand/data/media/tv. - Create your admin account and any additional user accounts.
- First-run wizard → choose Jellyfin as your media server, then
sign in using your Jellyfin server URL (
http://jellyfin:8096) and admin credentials. - Settings → Services → add Sonarr (
http://sonarr:8989) and Radarr (http://radarr:7878) with their API keys and the same root folders you set above.
Because every service here shares a Docker network with NPM, you don't need to publish ports to the public internet at all — NPM reaches them by container name over the internal Docker network. For each service, in NPM add a Proxy Host:
| Service | Forward Hostname | Forward Port | Suggested domain |
|---|---|---|---|
| Jellyfin | jellyfin |
8096 | jellyfin.yourdomain.com |
| Seerr | seerr |
5055 | requests.yourdomain.com |
| Sonarr | sonarr |
8989 | sonarr.yourdomain.com |
| Radarr | radarr |
7878 | radarr.yourdomain.com |
| Prowlarr | prowlarr |
9696 | prowlarr.yourdomain.com |
| qBittorrent | qbittorrent |
8080 | qbit.yourdomain.com |
For each Proxy Host: enable SSL (request a Let's Encrypt cert, force SSL), and enable Websockets Support — Jellyfin in particular needs this for live TV/sync features.
Jellyfin advanced tab — add this to avoid buffering issues on larger streams:
client_max_body_size 20M;
proxy_buffering off;Sonarr, Radarr, Prowlarr, and qBittorrent's WebUIs are management interfaces, not end-user apps — don't expose them to the open internet without an extra layer:
- Easiest: in NPM, create an Access List (Settings → Access Lists) with HTTP Basic Auth or an IP allowlist, and attach it to those four Proxy Hosts.
- Better: don't create public Proxy Hosts for them at all — reach them over a VPN/Tailscale connection to the VM instead, and only expose Jellyfin and Seerr publicly (they have their own user-facing auth and are designed to be internet-facing).
Either way, set strong unique passwords on all of them — Prowlarr and Sonarr/Radarr's APIs are a real target if left open with default creds.
- Imports copy instead of hardlink: confirm
/data/torrentsand/data/mediaare both under the sameDATA_ROOTmount — hardlinks don't work across different filesystems/mounts. - NPM shows 502: confirm the target container is on the same
Docker network as NPM (
docker network inspect <network>should list both).
If you get a WireGuard-capable VPN subscription down the line, the change is small:
- Add a
wireguardservice back todocker-compose.yml(config,NET_ADMIN/SYS_MODULEcaps, and your provider'swg0.confinconfig/wireguard/wg_confs/). - On
qbittorrent, remove its ownports:andnetworks:and addnetwork_mode: "service:wireguard"instead, so its traffic is forced through the tunnel. - Move qBittorrent's port mappings (
8080,6881) onto thewireguardservice instead. - Update Sonarr/Radarr's download client host from
qbittorrenttowireguard, and NPM's qBittorrent Proxy Host forward hostname the same way.