Arr Stack Docker Compose (Prowlarr, Radarr, Sonarr + VPN)
A complete docker-compose.yml for the *arr stack — Prowlarr, Radarr, Sonarr, and a VPN-shielded qBittorrent — with the single-root layout that makes imports instant hardlinks.
yaml# docker-compose.yml — full Arr stack. qBittorrent runs inside gluetun's
# VPN namespace (kill-switch); the Arr apps stay on the LAN. Every app
# mounts the SAME /data root so imports are instant hardlinks, not copies.
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 # set to YOUR LAN
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment: [PUID=1000, PGID=1000, TZ=Etc/UTC, WEBUI_PORT=8080]
volumes:
- /opt/qbittorrent:/config
- /data/torrents:/data/torrents
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
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
What this does
Stands up the full Arr stack in one file: Prowlarr (indexer manager), Radarr (movies), Sonarr (TV), and qBittorrent routed through a gluetun VPN container with a kill-switch. The step-by-step wiring (Prowlarr apps, indexers, download client, quality profiles) is in the Deploy the Arr Stack and Automation guides.
Why this approach
The Arr stack is four services that each handle one piece of automated media management. Prowlarr talks to indexers (the sources of NZB/torrent links) and syncs those indexers to Radarr and Sonarr automatically, so you only configure indexers in one place. Radarr tracks your movie wishlist and kicks off a download when something matching your quality profile appears. Sonarr does the same for TV series. qBittorrent (wrapped in gluetun) does the actual downloading. Together they go from “I want to watch this movie” to “file appears in Plex, ready to play” with no manual steps.
The gluetun VPN wrapper is the detail that makes the download client safe to run at home. Every byte of torrent traffic exits through the VPN — not your home IP. Gluetun runs as a container that owns the VPN tunnel, and qBittorrent is placed in gluetun’s network namespace with network_mode: service:gluetun. The result is that if the VPN connection drops for any reason, qBittorrent immediately loses all network access — the kill-switch is structural, not configured in qBittorrent’s settings. You can’t misconfigure it away.
The single-root /data layout — one top-level directory shared by all services as their data root — is the detail that makes library import instant. When Radarr imports a completed download, it moves the file from /data/downloads/movies/ to /data/media/movies/. If both paths are inside the same filesystem mount, that move is a hardlink operation (metadata update, no file copy) and completes instantly regardless of file size. If the downloads and media folders are on different mounts, every import is a full copy, which can take minutes for large files and doubles your storage usage temporarily. Getting the layout right before the first download saves significant frustration.
Prerequisites
- Docker + Docker Compose (install guide)
- A VPN provider supporting WireGuard + port forwarding (ProtonVPN, for example)
- The single-root
/datalayout created first (below)
Create the single-root layout
Downloads and media must share one filesystem or imports fall back to slow copies. Per the TRaSH Guides folder structure:
sudo mkdir -p /data/torrents/{movies,tv} /data/media/{movies,tv}
sudo chown -R 1000:1000 /data
Deploy
Save the script above as /opt/arr-stack/docker-compose.yml, fill in your WireGuard key and LAN subnet, then:
cd /opt/arr-stack
sudo docker compose up -d
# Verify torrent traffic exits the VPN, not your home IP:
sudo docker exec gluetun wget -qO- https://api.ipify.org ; echo
Then set each app’s root folder to /data/media/movies (Radarr) and /data/media/tv (Sonarr), and the download client category folders under /data/torrents/ — so imports hardlink instantly.
Notes
- Only qBittorrent is VPN-shielded — the Arr apps move metadata only.
FIREWALL_OUTBOUND_SUBNETSmust be your real LAN or the apps can’t reach the download client. Port-forward sync details are in the VPN Torrent Stack playbook. - Keep
PUID/PGIDidentical (1000 here) across all containers so imports don’t hit permission errors. - Add indexers only in Prowlarr — register Radarr/Sonarr as applications and it syncs them automatically.
- Never commit your real WireGuard key. Keep it on the host or in an
.envfile outside version control.