homelab-blueprint/docker-compose-files/arr-stack
Brian Pooe bba03c675a Fix arr-stack OOM kills and decluttarr auth for 25 GB downloads
- qbittorrent memory limit 1G → 3G: piece-verification buffers for 25 GB+
  torrents exceed 1G and trigger cgroup OOM kill mid-download
- bazarr, prowlarr, seerr limits 512M → 1G: all three were at 40-67% at
  idle with insufficient headroom for subtitle batch events, RSS floods,
  and Emby library syncs respectively
- Add *media-data-mount and storage-guard dependency to decluttarr so
  detect_deletions can access /data/media paths
- Fix decluttarr auth ban loop: add Docker bridge 172.18.0.0/16 to
  qBittorrent WebUI auth subnet whitelist (applied live via API)
- Update README mount table (Decluttarr now gets /data/media), memory
  limit table with per-service rationale sized for 25 GB average files
- Add TROUBLESHOOTING sections 5e-5j covering qBittorrent path issues,
  OOM kill pattern, and decluttarr auth ban loop with fixes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 17:52:19 +02:00
..
config refactor: organize by stack 2026-03-29 21:09:33 +02:00
README.md Fix arr-stack OOM kills and decluttarr auth for 25 GB downloads 2026-06-21 17:52:19 +02:00
template.yaml Fix arr-stack OOM kills and decluttarr auth for 25 GB downloads 2026-06-21 17:52:19 +02:00

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 before starting.

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

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:

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

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:

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

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:

DOCKERSTORAGEDIR=/mnt/unas/arr_data

*arr_data-mount expands to:

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:

# *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 and keeps download and library paths on one filesystem for hardlinks and atomic moves.

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.21.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 200300 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. 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:

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:

    cp -n .env.sample .env
    
  2. Set the Arr Stack values in .env:

    # 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=<airvpn-wireguard-private-key>
    WIREGUARD_PRESHARED_KEY=<airvpn-wireguard-preshared-key>
    WIREGUARD_ADDRESSES=<airvpn-wireguard-address>
    FIREWALL_VPN_INPUT_PORTS=<forwarded-port-or-empty>
    SERVER_COUNTRIES=<preferred-country-or-empty>
    
    # 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:

    VPN_SERVICE_PROVIDER=custom
    WIREGUARD_PRIVATE_KEY=<from-airvpn-wireguard-config>
    WIREGUARD_PRESHARED_KEY=<from-airvpn-wireguard-config>
    WIREGUARD_ADDRESSES=<from-airvpn-wireguard-config>
    WIREGUARD_PUBLIC_KEY=<peer-PublicKey-from-airvpn-wireguard-config>
    WIREGUARD_ENDPOINT_IP=<resolved-ip-of-server-hostname>
    WIREGUARD_ENDPOINT_PORT=<port-from-airvpn-wireguard-config>
    FIREWALL_VPN_INPUT_PORTS=<forwarded-port-or-empty>
    # 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 <hostname> 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:

    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:

    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:

    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://<HOST_IP>:8989.
    • Open Radarr at http://<HOST_IP>: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:

    SONARR_API_KEY=<sonarr-api-key>
    RADARR_API_KEY=<radarr-api-key>
    SABNZBD_API_KEY=<sabnzbd-api-key-or-empty>
    QBITTORRENT_USERNAME=<username-or-empty>
    QBITTORRENT_PASSWORD=<password-or-empty>
    
  7. Render the final Compose file and Recyclarr configuration:

    ./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:

    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:

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

./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 for storage and permissions and the troubleshooting guide for stack issues.

Useful Commands

docker compose -f docker-compose.arr-stack.yml logs --tail=100
docker compose -f docker-compose.arr-stack.yml restart <service>

Backup

Back up DOCKERCONFDIR. Shared media and downloads live in DOCKERSTORAGEDIR.