Deploy the Arr Stack with Docker Compose (Prowlarr, Radarr, Sonarr)

Stand up the full *arr stack in Docker Compose — Prowlarr, Radarr, Sonarr, and a VPN-shielded download client — with the single-root folder layout that enables instant hardlink imports.

The Arr stack is a handful of containers that work best when they’re wired together correctly. This guide stands up the whole thing in one Docker Compose file — Prowlarr, Radarr, Sonarr, and a VPN-shielded qBittorrent — with the folder layout that makes imports instant instead of slow.

Run it wherever Docker lives: a NAS, a Docker-on-Proxmox LXC or VM, or a dedicated Linux box. Once it’s up, the automation guide connects it to your media server.


Task 1: Get the folder layout right first

This is the single most important decision, and the most common mistake. Downloads and your media library must live under one shared root, mounted identically into every container. When they do, Radarr and Sonarr import files as instant hardlinks — the file appears in the library immediately, taking no extra disk space. When they’re split into separate mounts, every import is a slow copy that doubles disk usage.

The TRaSH Guides recommended layout uses a single /data root:

Single-root layout on the host

/data
├── torrents          # download client writes here
│   ├── movies
│   └── tv
└── media             # media server reads here
  ├── movies
  └── tv

Every container mounts the parent /data, not the subfolders:

The mistake that breaks hardlinks

Do NOT mount /downloads and /movies as separate volumes. To the container that’s two filesystems, so imports fall back to copy-and-delete. Mount one shared parent (/data) into every app. Hardlinks and atomic moves require everything on one filesystem — and every app needs read access, while the importing apps need write access.

Create it with correct ownership (use the same UID/GID you’ll give the containers):

Create the data tree

sudo mkdir -p /data/torrents/{movies,tv} /data/media/{movies,tv}
sudo chown -R 1000:1000 /data

Task 2: Write the Compose file

This runs Prowlarr, Radarr, and Sonarr on the LAN, with qBittorrent inside a gluetun VPN container so only the torrent traffic is shielded. The images come from LinuxServer.io, whose containers are the community standard for the Arr apps and share a consistent PUID/PGID permissions model. The full file is also available as a copy-paste Arr stack Compose playbook.

docker-compose.yml

services:
gluetun:
  image: qmcgaw/gluetun:latest
  container_name: gluetun
  cap_add: [NET_ADMIN]
  devices: [/dev/net/tun:/dev/net/tun]
  ports:
    - "8080:8080"        # qBittorrent WebUI, published by gluetun
  environment:
    - VPN_SERVICE_PROVIDER=protonvpn
    - VPN_TYPE=wireguard
    - WIREGUARD_PRIVATE_KEY=YOUR_WIREGUARD_PRIVATE_KEY
    - SERVER_COUNTRIES=Netherlands
    - VPN_PORT_FORWARDING=on
    - FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
  restart: unless-stopped

qbittorrent:
  image: lscr.io/linuxserver/qbittorrent:latest
  container_name: qbittorrent
  network_mode: "service:gluetun"      # all traffic through the VPN
  environment: [PUID=1000, PGID=1000, TZ=Etc/UTC, WEBUI_PORT=8080]
  volumes:
    - /opt/qbittorrent:/config
    - /data/torrents:/data/torrents     # same path as the Arr apps
  depends_on: [gluetun]
  restart: unless-stopped

prowlarr:
  image: lscr.io/linuxserver/prowlarr:latest
  container_name: prowlarr
  ports: ["9696:9696"]
  environment: [PUID=1000, PGID=1000, TZ=Etc/UTC]
  volumes: [/opt/prowlarr:/config]
  restart: unless-stopped

radarr:
  image: lscr.io/linuxserver/radarr:latest
  container_name: radarr
  ports: ["7878:7878"]
  environment: [PUID=1000, PGID=1000, TZ=Etc/UTC]
  volumes:
    - /opt/radarr:/config
    - /data:/data                        # ONE shared root -> hardlinks
  restart: unless-stopped

sonarr:
  image: lscr.io/linuxserver/sonarr:latest
  container_name: sonarr
  ports: ["8989:8989"]
  environment: [PUID=1000, PGID=1000, TZ=Etc/UTC]
  volumes:
    - /opt/sonarr:/config
    - /data:/data
  restart: unless-stopped
Set FIREWALL_OUTBOUND_SUBNETS to your real LAN

gluetun blocks everything except the VPN by default. qBittorrent needs to talk to Radarr/Sonarr on your LAN, so FIREWALL_OUTBOUND_SUBNETS must match your actual subnet or the Arr apps can’t reach the client. Full details and port-forward sync are in the VPN Torrent Stack playbook.

Bring it up and confirm the VPN is actually shielding traffic:

Deploy and verify the tunnel

sudo docker compose up -d

# This MUST print a VPN IP, not your home IP:
sudo docker exec gluetun wget -qO- https://api.ipify.org ; echo

Task 3: Wire the apps together

The containers are running but don’t know about each other yet. Three connections to make, all in the web UIs.

1Register Sonarr and Radarr in Prowlarr5 min

Grab each app’s API key from its Settings → General page. In Prowlarr → Settings → Apps → Add, add Radarr and Sonarr, pasting each API key and the container URL (http://radarr:7878, http://sonarr:8989 — containers reach each other by service name). Prowlarr will now sync indexers to both.

2Add indexers in Prowlarr5 min

In Prowlarr → Indexers → Add Indexer, add the trackers or Usenet indexers you use. They sync to Radarr and Sonarr automatically — you never touch indexer settings in those apps.

3Add qBittorrent as the download client5 min

In Radarr → Settings → Download Clients → Add → qBittorrent (and again in Sonarr), point it at the qBittorrent host and port (http://<host-ip>:8080, since qBittorrent’s UI is published by gluetun on the host). Set a categoryradarr in Radarr, sonarr in Sonarr — so downloads land in the right subfolder.

Tip

Set each app’s root folder to the media subpath — /data/media/movies in Radarr, /data/media/tv in Sonarr. Because the download client writes to /data/torrents/... under the same /data mount, imports become instant hardlinks exactly as intended.


Task 4: Test the pipeline

Add one movie in Radarr, mark it monitored, and hit Search. Watch it flow: Radarr queries Prowlarr’s indexers → picks a release → sends it to qBittorrent → and on completion, imports it into /data/media/movies as a hardlink. If the import is instant and disk usage doesn’t jump, your folder layout is correct.

Ownership gotcha

If a download completes but Radarr/Sonarr won’t import it, it’s almost always permissions — the file is owned by a user the app can’t read or move. Keep the same PUID/PGID (1000 here) across qBittorrent and the Arr apps, and make sure /data is owned by that user.


What’s next

You have a working stack: add a title, it downloads and files itself. The automation guide takes it hands-off — import lists that add titles for you, quality profiles that pick the best release, and a notification that rescans your media server the moment a file lands.


Related posts:

Sources: TRaSH Guides — Hardlinks and Instant Moves, Servarr Docker Guide.