# Arr Stack Docker media stack with Gluetun, download clients, media managers, Emby, Recyclarr, Unpackerr, and Decluttarr. Run repository commands from the project root. ## Appdata Location This stack is the exception to project-root `appdata/`: - `DOCKERCONFDIR` stores application configs on local disk. - `DOCKERSTORAGEDIR` stores shared downloads and media. The default paths are `/opt/arr-stack/appdata` and `/mnt/unas/arr_data`. Complete [the storage deployment guide](../../docs/arr-stack/deployment.md) before starting. ## Recommended LXC Mode - **Privileged LXC:** Required for NVIDIA GPU passthrough (Emby) and easier NFS mapping. - **Unprivileged LXC:** Supported for CPU-only transcoding, but requires manual UID/GID mapping for NFS write access. ## UNAS Pro Shared-Drive Structure Create one UNAS Pro shared drive for the entire Arr data root, for example `arr_data`, and enable NFS for that shared drive. Mount its NFS export on the Proxmox host at `/mnt/unas/arr_data`, then bind-mount that same path into the LXC. The **root of the UNAS shared drive** must contain the marker and the three TRaSH-style directories: ```text arr_data/ # UNAS shared-drive root |-- .arr-stack-storage |-- torrents/ | |-- incomplete/ | |-- movies/ | `-- tv/ |-- usenet/ | |-- incomplete/ | `-- complete/ | |-- movies/ | `-- tv/ `-- media/ |-- movies/ `-- tv/ ``` When that shared-drive root is mounted at `/mnt/unas/arr_data`, the LXC must see: ```text /mnt/unas/arr_data/.arr-stack-storage /mnt/unas/arr_data/torrents/incomplete /mnt/unas/arr_data/torrents/movies /mnt/unas/arr_data/torrents/tv /mnt/unas/arr_data/usenet/incomplete /mnt/unas/arr_data/usenet/complete/movies /mnt/unas/arr_data/usenet/complete/tv /mnt/unas/arr_data/media/movies /mnt/unas/arr_data/media/tv ``` Do not create another nested `/mnt/unas/arr_data/arr_data` directory. All download and library directories must remain on this one NFS export and filesystem so hardlinks and atomic moves can work. Create the tree from inside the LXC as the configured `PUID`: ```bash ARR_USER="$(getent passwd 1000 | cut -d: -f1)" su -s /bin/bash -c ' mkdir -p \ /mnt/unas/arr_data/torrents/{incomplete,movies,tv} \ /mnt/unas/arr_data/usenet/{incomplete,complete/{movies,tv}} \ /mnt/unas/arr_data/media/{movies,tv} touch /mnt/unas/arr_data/.arr-stack-storage ' "$ARR_USER" ``` Use one consistent mapped owner/group and the TRaSH-recommended `775` directory / `664` file permissions: ```bash find /mnt/unas/arr_data -type d -exec chmod 775 {} + find /mnt/unas/arr_data -type f -exec chmod 664 {} + ``` This guide assumes a **Privileged LXC** where UID `1000` inside the container maps 1:1 to UID `1000` on the Proxmox host. Set ownership from the **Proxmox host** (not from inside the LXC): ```bash chown -R 1000:1000 /mnt/unas/arr_data ``` This requires the `arr_data` share to have **No Root Squash** (Isolated Mode) set in UniFi Drive. Without it, the Proxmox host root is squashed to an anonymous UID (977:988) that does not own the export and cannot change ownership. See `docs/unas/nfs-mounts.md` step 1 for how to set this. ## TRaSH-Aligned Container Mounts The Compose template uses YAML anchors to keep long bind-mount definitions consistent. After setting: ```dotenv DOCKERSTORAGEDIR=/mnt/unas/arr_data ``` `*arr_data-mount` expands to: ```yaml type: bind source: /mnt/unas/arr_data target: /data bind: create_host_path: false ``` This full-root mount is used by the storage guard, Radarr, and Sonarr. These Starr apps must see downloads and media through the same `/data` mount for hardlinks and atomic moves. The restricted anchors expand as follows: ```yaml # *torrents-data-mount source: /mnt/unas/arr_data/torrents target: /data/torrents # *usenet-data-mount source: /mnt/unas/arr_data/usenet target: /data/usenet # *media-data-mount source: /mnt/unas/arr_data/media target: /data/media ``` | Service | Shared-data mount inside container | |---|---| | Storage guard | `/data` | | Radarr, Sonarr | `/data` | | qBittorrent | `/data/torrents` | | SABnzbd | `/data/usenet` | | Bazarr, Emby, Decluttarr | `/data/media` | | Unpackerr | `/data/torrents` and `/data/usenet` | | Prowlarr, FlareSolverr, Seerr, Recyclarr | None | Do not add separate `/movies`, `/tv`, or `/downloads` mounts. Inside the apps, use `/data/torrents/...`, `/data/usenet/...`, and `/data/media/...`. This layout follows the [TRaSH Guides Docker folder structure](https://trash-guides.info/File-and-Folder-Structure/How-to-set-up/Docker/) and keeps download and library paths on one filesystem for [hardlinks and atomic moves](https://trash-guides.info/File-and-Folder-Structure/Hardlinks-and-Instant-Moves/). ## Recommended LXC Allocation The current Compose template reserves approximately `4.1 GB` of memory across the stack and permits approximately `15.3 GB` at the combined container limits. Emby, SABnzbd unpacking, qBittorrent, and library scans create the largest resource spikes. | Resource | Recommended starting point | Minimum | |---|---:|---:| | CPU | 4 cores | 2 cores | | RAM | 8 GB | 6 GB | | Swap | 2 GB | 1 GB | | LXC root disk | 64 GB | 32 GB | | GPU | NVIDIA RTX A400 passthrough for Emby | Optional | Container memory limits (sized for 25 GB average downloads): | Service | Limit | Reservation | Notes | |---|---:|---:|---| | gluetun | 512M | 128M | WireGuard userspace daemon; stable | | qbittorrent | 3G | 256M | Piece-verification buffers at high progress for 25 GB+ torrents require >1G | | flaresolverr | 1G | 256M | Headless Chromium; short-lived per-request spikes | | sabnzbd | 2G | 512M | par2 repair of 25 GB NZBs can spike 1.2–1.8 GiB | | prowlarr | 1G | 256M | RSS sync across many indexers spikes the 198 MiB idle baseline | | radarr | 1G | 256M | Library scans are I/O-bound; 1G comfortable | | sonarr | 1G | 256M | Same as Radarr | | bazarr | 1G | 256M | 345 MiB at idle (67% of old 512M); subtitle batch on new 4K imports adds 200–300 MiB | | emby | 3G | 1G | NVIDIA HW transcode keeps RAM library-metadata-bound; 3G comfortable | | seerr | 1G | 256M | 216 MiB at idle; Emby library sync on startup spikes to ~450 MiB | | recyclarr | 256M | 64M | Scheduled sync tool; peaks <80 MiB | | unpackerr | 512M | 128M | Shells out to system unrar/7z; own RSS stays <50 MiB | | decluttarr | 512M | 128M | Polling daemon; no large data structures | > **qBittorrent memory note:** piece-verification buffers for large torrents (25 GB+) > can push qBittorrent well past 1 GiB during active download. The template sets a > 3 GiB limit. Do not lower it below 2 GiB or the cgroup OOM killer will terminate > the process mid-download, causing the torrent to appear to restart from a lower > percentage. Use `docker update --memory 3g --memory-swap 6g qbittorrent` to apply > the limit live without interrupting an in-progress download. Use `12 GB` RAM and 6 CPU cores for multiple simultaneous Emby streams, large libraries, heavy SABnzbd unpacking, or hundreds of active torrents. The `emby` service is configured to use the `nvidia` runtime; ensure the Proxmox host drivers and the NVIDIA Container Toolkit are installed and configured according to the [NVIDIA GPU Passthrough guide](../../docs/nvidia/proxmox-nvidia-lxc.md). Without GPU passthrough, software transcoding can consume all available CPU; avoid transcoding or allocate substantially more CPU. Downloads and media live on `DOCKERSTORAGEDIR`, so the LXC root disk primarily stores Docker images, logs, application configs, SQLite databases, and Emby metadata. Allocate `100 GB` instead of `64 GB` if Emby generates extensive thumbnails or chapter previews. After several days of representative usage, check actual peaks: ```bash docker stats --no-stream free -h df -h / ``` Increase LXC RAM if usage regularly approaches the allocation or swap usage continues growing. ## Installation 1. Create `.env` if it does not exist: ```bash cp -n .env.sample .env ``` 2. Set the Arr Stack values in `.env`: ```dotenv # Local LXC config storage and NFS-backed shared media/download storage DOCKERCONFDIR=/opt/arr-stack/appdata DOCKERSTORAGEDIR=/mnt/unas/arr_data ARR_DATA_MARKER=.arr-stack-storage # Container identity; must match storage ownership PUID=1000 PGID=1000 TZ=Africa/Johannesburg # Docker logs DOCKERLOGGING_MAXFILE=3 DOCKERLOGGING_MAXSIZE=10m # Gluetun / AirVPN WireGuard VPN_SERVICE_PROVIDER=airvpn WIREGUARD_PRIVATE_KEY= WIREGUARD_PRESHARED_KEY= WIREGUARD_ADDRESSES= FIREWALL_VPN_INPUT_PORTS= SERVER_COUNTRIES= # Required after initial Sonarr/Radarr setup SONARR_API_KEY= RADARR_API_KEY= # Optional integrations SABNZBD_API_KEY= QBITTORRENT_BASE_URL=http://gluetun:8080 QBITTORRENT_USERNAME= QBITTORRENT_PASSWORD= # Decluttarr queue cleanup DECLUTTARR_TEST_RUN=True DECLUTTARR_TIMER=10 DECLUTTARR_MAX_STRIKES=3 DECLUTTARR_LOG_LEVEL=INFO DECLUTTARR_DETECT_DELETIONS=True ``` `DOCKERCONFDIR` must be local LXC storage. `DOCKERSTORAGEDIR` must be the mounted shared-data path and must contain `ARR_DATA_MARKER`. Obtain the WireGuard values and forwarded port from AirVPN. Leave `FIREWALL_VPN_INPUT_PORTS` empty if no inbound forwarded port is configured. `SERVER_COUNTRIES` may also remain empty. **AirVPN preshared key warning:** AirVPN generates a unique preshared key for each device-server combination. The built-in `airvpn` Gluetun provider selects a server dynamically, which will not match the preshared key in your `.env`. This causes a silent WireGuard failure — the tunnel appears to connect but DNS lookups time out through it. If you see this symptom, use `VPN_SERVICE_PROVIDER=custom` and pin a specific server instead: ```dotenv VPN_SERVICE_PROVIDER=custom WIREGUARD_PRIVATE_KEY= WIREGUARD_PRESHARED_KEY= WIREGUARD_ADDRESSES= WIREGUARD_PUBLIC_KEY= WIREGUARD_ENDPOINT_IP= WIREGUARD_ENDPOINT_PORT= FIREWALL_VPN_INPUT_PORTS= # Do NOT set SERVER_COUNTRIES or SERVER_HOSTNAMES — the custom provider rejects them ``` All four WireGuard values (`PrivateKey`, `PresharedKey`, `Address`, `PublicKey`, and `Endpoint`) come from the WireGuard config file downloaded from the AirVPN client area for a specific server. The `WIREGUARD_ENDPOINT_IP` is the resolved IP address of the server hostname in the `Endpoint` field (use `dig +short ` to resolve it). Leave `SERVER_COUNTRIES` empty or absent. Sonarr and Radarr API keys do not exist until those applications complete their first startup. Leave them empty during the bootstrap phase below. Keep `DECLUTTARR_TEST_RUN=True` until its actions have been reviewed; change it to `False` only when ready to enable queue cleanup. 3. Create config directories, verify shared storage, and render the initial Compose file: ```bash sudo mkdir -p /opt/arr-stack/appdata/{gluetun,qbittorrent,sabnzbd,prowlarr,radarr,sonarr,bazarr,emby,seerr,recyclarr,unpackerr} sudo chown -R 1000:1000 /opt/arr-stack sudo -u '#1000' touch /mnt/unas/arr_data/.arr-stack-storage ./substitute_env.sh docker-compose-files/arr-stack/template.yaml docker-compose.arr-stack.yml .env ``` Do not render `recyclarr.yml` yet. Empty API keys produce an unusable Recyclarr configuration. 4. Validate Compose and start only the bootstrap services: ```bash docker compose -f docker-compose.arr-stack.yml config --quiet docker compose -f docker-compose.arr-stack.yml up -d \ storage-guard gluetun qbittorrent flaresolverr sabnzbd prowlarr \ radarr sonarr bazarr emby seerr ``` This intentionally leaves Recyclarr, Unpackerr, and Decluttarr stopped until their API credentials are available. 5. Verify the bootstrap services and VPN: ```bash docker compose -f docker-compose.arr-stack.yml ps docker exec gluetun wget -qO- https://ipinfo.io/ip ``` 6. Complete initial Sonarr and Radarr setup: - Open Sonarr at `http://:8989`. - Open Radarr at `http://:7878`. - Configure their root folders and download clients as described under **Initial Setup** below. - Copy each API key from **Settings > General > Security**. - If using Decluttarr with SABnzbd or authenticated qBittorrent, also collect those credentials. Update `.env`: ```dotenv SONARR_API_KEY= RADARR_API_KEY= SABNZBD_API_KEY= QBITTORRENT_USERNAME= QBITTORRENT_PASSWORD= ``` 7. Render the final Compose file and Recyclarr configuration: ```bash ./substitute_env.sh docker-compose-files/arr-stack/template.yaml docker-compose.arr-stack.yml .env ./substitute_env.sh \ docker-compose-files/arr-stack/config/recyclarr_template.yml \ /opt/arr-stack/appdata/recyclarr/recyclarr.yml \ .env sudo chown 1000:1000 /opt/arr-stack/appdata/recyclarr/recyclarr.yml sudo chmod 640 /opt/arr-stack/appdata/recyclarr/recyclarr.yml docker compose -f docker-compose.arr-stack.yml config --quiet ``` The rendered Recyclarr configuration uses Docker service URLs `http://sonarr:8989` and `http://radarr:7878`. Do not replace them with `localhost`; Recyclarr runs in its own container. 8. Start Recyclarr and preview its TRaSH Guides changes: ```bash docker compose -f docker-compose.arr-stack.yml up -d recyclarr docker compose -f docker-compose.arr-stack.yml exec recyclarr recyclarr config list docker compose -f docker-compose.arr-stack.yml exec recyclarr recyclarr sync --preview ``` Review the preview before applying it. The repository template creates the documented Sonarr and Radarr quality profiles, quality definitions, custom formats, and scores. 9. Apply Recyclarr and start the remaining services: ```bash docker compose -f docker-compose.arr-stack.yml exec recyclarr recyclarr sync docker compose -f docker-compose.arr-stack.yml up -d docker compose -f docker-compose.arr-stack.yml ps ``` See [Recyclarr Configuration Guide](../../docs/arr-stack/recyclarr.md) before changing the supplied quality profiles or custom-format scores. ## Apply Configuration Changes After changing `.env` or a template, render the affected file again and recreate the stack: ```bash ./substitute_env.sh docker-compose-files/arr-stack/template.yaml docker-compose.arr-stack.yml .env ./substitute_env.sh docker-compose-files/arr-stack/config/recyclarr_template.yml /opt/arr-stack/appdata/recyclarr/recyclarr.yml .env sudo chown 1000:1000 /opt/arr-stack/appdata/recyclarr/recyclarr.yml sudo chmod 640 /opt/arr-stack/appdata/recyclarr/recyclarr.yml docker compose -f docker-compose.arr-stack.yml config --quiet docker compose -f docker-compose.arr-stack.yml exec recyclarr recyclarr sync --preview docker compose -f docker-compose.arr-stack.yml up -d ``` Apply `recyclarr sync` only after reviewing the preview. ## Initial Setup After starting the stack, configure the applications using these internal container paths. Do not add separate `/movies`, `/tv`, or `/downloads` mounts. | Application | Path | |---|---| | qBittorrent default save path | `/data/torrents` | | qBittorrent incomplete path | `/data/torrents/incomplete` | | qBittorrent categories | `movies` → `/data/torrents/movies`; `tv` → `/data/torrents/tv` | | SABnzbd | `/data/usenet/incomplete`, `/data/usenet/complete` | | Radarr | `/data/media/movies` | | Sonarr | `/data/media/tv` | | Bazarr / Emby | `/data/media/movies`, `/data/media/tv` | In Radarr and Sonarr, configure qBittorrent at `gluetun:8080` and SABnzbd at `sabnzbd:8282`. Set Radarr's category to `movies` and Sonarr's category to `tv`. No remote path mapping is needed because all applications use the same container paths. In **Settings > Media Management**, enable **Use Hardlinks instead of Copy** in both applications. Hardlinks apply to torrent imports so the original can remain available for seeding. Usenet imports should use atomic moves from `/data/usenet/complete/movies` or `/data/usenet/complete/tv` into the media library because those paths are on the same filesystem. In Prowlarr, assign indexers to an app profile with RSS, automatic search, and interactive search enabled. The built-in `Standard` profile provides all three. An `Automatic Search` profile has RSS disabled and causes Radarr and Sonarr to report that no RSS-enabled indexers are available. After changing an indexer profile, use **Settings > Apps > Sync App Indexers** and confirm the synchronized indexers have RSS enabled. ## Network Requirements - The host needs `/dev/net/tun` for Gluetun. - Download clients require outbound VPN access. - Caddy or trusted clients need access to the published application ports. ## Troubleshooting Use [the deployment guide](../../docs/arr-stack/deployment.md) for storage and permissions and [the troubleshooting guide](../../docs/troubleshooting.md) for stack issues. ## Useful Commands ```bash docker compose -f docker-compose.arr-stack.yml logs --tail=100 docker compose -f docker-compose.arr-stack.yml restart ``` ## Backup Back up `DOCKERCONFDIR`. Shared media and downloads live in `DOCKERSTORAGEDIR`.