LinuxcontainersTested on real hardware

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.

Distrosubuntu, debian
Shellyaml
Updated
Compose file
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 /data layout 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:

bash

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:

bash

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_SUBNETS must 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/PGID identical (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 .env file outside version control.