# 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**](https://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](https://dev.to/josh_hall_b54941047f33661/what-is-the-arr-stack-sonarr-radarr-and-prowlarr-explained-4ocf).

*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](https://trash-guides.info/File-and-Folder-Structure/Hardlinks-and-Instant-Moves/) layout uses a single `/data` root:

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

**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:

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

## 2\. The Compose file

Prowlarr, Radarr, and Sonarr run normally on your LAN. qBittorrent lives inside a [gluetun](https://github.com/qdm12/gluetun) VPN container, so the torrent traffic, and only the torrent traffic, goes through the VPN. The images are from [LinuxServer.io](https://www.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.

```yaml
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
```

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:

```bash
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
```

## 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](https://peira.dev/_astro/arr-prowlarr-apps.VikqLEc5_Z24CcI0.webp align="center")

**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](https://peira.dev/_astro/arr-radarr-download-clients.B-5qDZ7m_2i0gUv.webp align="center")

**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](https://peira.dev/_astro/arr-radarr-root-folders.CgLcAvUC_ZN6VtS.webp align="center")

## 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

*   Copy-paste version: the [Arr stack Compose playbook](https://peira.dev/playbooks/arr-stack-docker-compose/)
    
*   The download client in depth, with kill-switch and port-forward sync: the [VPN torrent stack playbook](https://peira.dev/playbooks/vpn-torrent-stack-docker/)
    
*   Take it hands-off with import lists, quality profiles, and a media-server rescan: [Automating the Arr Stack](https://peira.dev/articles/arr-stack-automation/)
    

Sources: [TRaSH Guides: Hardlinks and Instant Moves](https://trash-guides.info/File-and-Folder-Structure/Hardlinks-and-Instant-Moves/) · [Servarr Docker Guide](https://wiki.servarr.com/docker-guide)

*Written by Peira Labs. Full version, with the screenshots and FAQ, at* [*peira.dev*](https://peira.dev/articles/arr-stack-docker-deploy/)*.*
