homelab-blueprint/docker-compose-files/arr-stack/README.md
Brian Pooe e02f79092f Fix arr-stack docs for real-world UNAS NFSv3 constraints
Correct DEPLOYMENT.md fstab entry (vers=3, UUID path, 10.0.10.25) and
add No Root Squash requirement note to the chown step. Fix arr-stack
README to document host-side chown from a privileged LXC instead of
the incorrect unprivileged-LXC warning. Also carries forward nfs-mounts.md
from the previous session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 14:23:25 +02:00

13 KiB

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/
|   |-- 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/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/{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 /data/media
Unpackerr /data/torrents and /data/usenet
Prowlarr, FlareSolverr, Seerr, Recyclarr, Decluttarr 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 3.4 GB of memory across the stack and permits approximately 12.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

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.

    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 /data/torrents/movies, /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. Use matching categories; no remote path mapping is needed.

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.