DEV Community

Cover image for Deploy the Arr Stack with Docker Compose (Prowlarr, Radarr, Sonarr)
Josh Hall
Josh Hall

Posted on Originally published at peira.dev

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

The short version: put your downloads and your media library under one shared root (/data), mount that same root into every container, and run only the torrent client behind a VPN container. Get the folder layout right and every import is an instant hardlink; get it wrong and every import is a slow copy that doubles your disk usage. Everything below is one Compose file plus three connections in the web UIs.

Cross-post from Peira Labs. Full version with screenshots at every step: peira.dev/articles/arr-stack-docker-deploy

New to the stack? Start with the concepts: What Is the Arr Stack? Sonarr, Radarr, and Prowlarr Explained.

Lawful use: this automates media you have the rights to, such as your own rips, DRM-free purchases, home video, Linux ISOs, and public-domain or Creative Commons releases. Downloading copyrighted material you haven't paid for is illegal in most places, and it isn't what this guide teaches.

1. Get the folder layout right first

This is the single most important decision in the build, and the one you can't easily fix later. When downloads and media sit on the same filesystem, Radarr and Sonarr import a finished download as a hardlink: a second directory entry pointing at the same data on disk. It appears in your library instantly, costs no extra space, and the original keeps seeding. Split them into separate mounts and the containers see two filesystems, so every import falls back to copy-and-delete.

The TRaSH Guides layout uses a single /data root:

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

Don't mount /downloads and /movies as separate volumes. Mount the parent /data into every app. Create it with the UID/GID you'll give the containers:

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

2. The Compose file

Prowlarr, Radarr, and Sonarr run normally on your LAN. qBittorrent lives inside a gluetun VPN container, so the torrent traffic, and only the torrent traffic, goes through the VPN. The images are from LinuxServer.io, which share one PUID/PGID permissions model.

Make these values your own first: YOUR_WIREGUARD_PRIVATE_KEY (from your VPN provider's WireGuard config; keep it in a secret store), protonvpn and Netherlands (your provider and server country), 192.168.1.0/24 (your real LAN subnet), 1000/1000 (the UID/GID that owns /data; check with id), and the /opt/... config paths.

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
Enter fullscreen mode Exit fullscreen mode

gluetun blocks everything except the VPN by default, so FIREWALL_OUTBOUND_SUBNETS must match your real LAN or the Arr apps can't reach qBittorrent. Bring it up and prove the tunnel works:

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
Enter fullscreen mode Exit fullscreen mode

3. Wire the apps together, in this order

Register Sonarr and Radarr in Prowlarr. Copy each app's API key from its Settings → General page, then add both in Prowlarr → Settings → Apps using the container URLs (http://radarr:7878, http://sonarr:8989). Containers reach each other by service name.

Prowlarr Settings, Apps: Radarr and Sonarr registered, each with a green Full Sync badge

Add indexers in Prowlarr only. They sync to Radarr and Sonarr automatically, so you never maintain two lists.

Add qBittorrent as the download client in Radarr and in Sonarr: Settings → Download Clients → qBittorrent at http://<host-ip>:8080 (gluetun publishes the UI on the host), with a category of radarr or sonarr so downloads land in the right subfolder.

Radarr Settings, Download Clients: qBittorrent enabled

Set each root folder to the media subpath: /data/media/movies in Radarr, /data/media/tv in Sonarr. Because qBittorrent writes to /data/torrents/... under the same mount, imports become hardlinks.

Radarr Media Management: a single root folder on the media path

4. Prove it end to end

Add one movie in Radarr, mark it monitored, and hit Search. Radarr queries Prowlarr's indexers, picks a release by its quality settings, hands it to qBittorrent, and imports the finished file into /data/media/movies. Success looks like this: the import completes instantly and your disk usage doesn't jump by the file's size. If either is off, look at the folder layout from step 1 first.

If a download finishes but won't import, it's almost always ownership. Keep the same PUID/PGID across qBittorrent and the Arr apps, and make sure /data belongs to that user.

Go further

Sources: TRaSH Guides: Hardlinks and Instant Moves · Servarr Docker Guide

Written by Peira Labs. Full version, with the screenshots and FAQ, at peira.dev.

Top comments (0)