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
Shellbash
Updated
Script
bash
# 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.

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.