homelab-blueprint/docs/arr-stack/deployment.md
Brian Pooe 1815beadf4 refactor: restructure docs, sync stacks with Caddyfile, harden compose templates
- Add compose stacks for apps already proxied by Caddy: bento-pdf,
  it-tools, paperless-ngx, and beszel-hub (with Makefile targets)
- Remove the unused Vault stack and the drawio Caddyfile block
- Fix Caddyfile gramps upstream to port 80 to match the compose file
- Bring immich and gramps-web up to the shared template pattern
  (no-new-privileges, log rotation, memory limits, healthchecks)
- Single-quote vaultwarden SSO_AUDIENCE_TRUSTED so regex values like
  ^\d{18}$ render as valid YAML
- Move root docs into kebab-case topic folders, rebuild the docs index
  to cover every doc, and fix all broken links
2026-07-03 17:45:16 +02:00

9.7 KiB

Proxmox LXC arr-stack Deployment

This walkthrough deploys the arr stack inside a Docker LXC. While unprivileged is the security default, this guide assumes a Privileged LXC if you intend to use NVIDIA GPU acceleration for Emby.

  • app configs and SQLite databases on local LXC disk at /opt/arr-stack/appdata;
  • downloads and media on a UniFi UNAS Pro NFS share;
  • NFS mounted only on the Proxmox host;
  • the NFS-backed directory bind-mounted into the LXC at /mnt/unas/arr_data;
  • the full shared root visible to Radarr and Sonarr at /data.

Privileged vs Unprivileged

  • Privileged (Recommended for GPU): Required for seamless NVIDIA driver access and CUDA compute. Root inside the LXC maps directly to root on the host.
  • Unprivileged (Standard Security): Safer, but requires complex UID mapping for GPU and NFS write access.

The storage path is:

UNAS NFS export
  -> Proxmox host: /mnt/unas/arr_data
  -> LXC: /mnt/unas/arr_data
  -> Docker containers: /data

Do not start the Compose stack until the storage verification steps pass.

Values Used in This Guide

Replace these examples with your real values:

Name Example Meaning
UNAS_IP 10.0.10.25 UNAS Pro IP address
PVE_IP 10.0.10.10 Proxmox host IP allowed by the UNAS
VMID 120 Docker LXC container ID
NFS_EXPORT /volume/b69055de-0b30-4e8c-b7ef-b533d92733c3/.srv/.unifi-drive/arr_data/.data Real export from showmount
HOST_DATA /mnt/unas/arr_data NFS mountpoint on Proxmox
LXC_DATA /mnt/unas/arr_data Bind-mount destination inside the LXC
CONFIG_ROOT /opt/arr-stack/appdata Local LXC config path
PUID:PGID 1000:1000 Identity used by the containers

Run Proxmox commands from the Proxmox shell as root. Run LXC commands after entering the LXC with pct enter 120.

1. Enable NFS on the UNAS

Why: the Proxmox host needs permission to mount the shared drive. The LXC must not mount NFS itself.

Step 1 — Enable the NFS service globally:

  1. Open the UniFi Drive web interface.
  2. Go to SettingsServices.
  3. Enable NFS and save.

This one-time toggle must be done before per-share NFS settings appear.

Step 2 — Enable NFS on the arr_data share:

  1. Go to Shares and click arr_data.
  2. Under Sharing Protocols, enable NFS.
  3. Add the Proxmox host IP (10.0.10.10) to the allowed hosts list. Do not add the LXC IP — Proxmox is the NFS client.
  4. Set the squash mode to No Root Squash (Isolated Mode). This is required so the Proxmox host root can set ownership on the share in step 5. Without it, chown silently fails and containers cannot write.
  5. Grant read/write access and save.

2. Discover the Real NFS Export Path

On the Proxmox host, install the NFS client tools:

apt update
apt install -y nfs-common

Set the UNAS address and list its exports:

UNAS_IP=10.0.10.25
showmount -e "$UNAS_IP"

Copy the exact export path. It will follow the UUID-based format: /volume/b69055de-0b30-4e8c-b7ef-b533d92733c3/.srv/.unifi-drive/arr_data/.data

3. Mount NFS on the Proxmox Host

Set the values for this shell session:

UNAS_IP=10.0.10.25
NFS_EXPORT=/volume/b69055de-0b30-4e8c-b7ef-b533d92733c3/.srv/.unifi-drive/arr_data/.data
HOST_DATA=/mnt/unas/arr_data

Create the empty host mountpoint:

mkdir -p "$HOST_DATA"

Back up and edit /etc/fstab:

nano /etc/fstab

Add one line:

10.0.10.25:/volume/b69055de-0b30-4e8c-b7ef-b533d92733c3/.srv/.unifi-drive/arr_data/.data /mnt/unas/arr_data nfs vers=3,noatime,nofail,_netdev,x-systemd.automount,x-systemd.mount-timeout=10s 0 0

Apply and verify:

systemctl daemon-reload
mount -a
findmnt "$HOST_DATA"

4. Bind Mount the Host Path into the LXC

Stop the LXC before changing mountpoints:

VMID=120
pct stop "$VMID"

Confirm the LXC is configured correctly (privileged if using GPU):

pct config "$VMID"

Bind mount the host path:

pct set "$VMID" -mp0 /mnt/unas/arr_data,mp=/mnt/unas/arr_data

Start and enter the LXC:

pct start "$VMID"
pct enter "$VMID"

5. Understand UID/GID Mapping

Why: In a Privileged LXC, mapping is 1:1. UID 1000 inside the LXC is UID 1000 on the host. In an Unprivileged LXC, IDs are shifted.

This guide assumes container/LXC user 1000:1000.

Inside the LXC, create the user if it does not exist:

groupadd --gid 1000 arrstack
useradd --uid 1000 --gid 1000 --create-home --shell /bin/bash arrstack

Set ownership on the NFS-backed arr data from the Proxmox host:

chown -R 1000:1000 /mnt/unas/arr_data

Requirement: This only works if the arr_data share was configured with No Root Squash (Isolated Mode) in step 1. UniFi Drive's default squash mode maps the Proxmox host root to anonymous UID 977:988, which does not own the .data directory and cannot chown anything inside it. With No Root Squash the host root retains real root privileges on the UNAS and the command above succeeds.

Verify write access inside the LXC:

su -s /bin/sh -c 'touch /mnt/unas/arr_data/.lxc-write-test && rm /mnt/unas/arr_data/.lxc-write-test' arrstack

6. Prepare Local Config and Shared Data

Create local config directories on the LXC root disk:

CONFIG_ROOT=/opt/arr-stack/appdata
mkdir -p "$CONFIG_ROOT"/{gluetun,qbittorrent,sabnzbd,prowlarr,radarr,sonarr,bazarr,emby,seerr,recyclarr,unpackerr}
chown -R 1000:1000 /opt/arr-stack

Create the TRaSH-style shared directory tree:

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}' arrstack
touch /mnt/unas/arr_data/.arr-stack-storage
chown 1000:1000 /mnt/unas/arr_data/.arr-stack-storage

7. Configure Paths Inside the Applications

All applications must use paths under the same /data prefix for hardlinks to work.

Application Path
qBittorrent default save path /data/torrents
qBittorrent incomplete path /data/torrents/incomplete
qBittorrent movies category /data/torrents/movies
qBittorrent tv category /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

Use category movies in Radarr and category tv in Sonarr. Do not configure remote path mappings when the applications use these shared paths.

In Settings > Media Management, enable Use Hardlinks instead of Copy in both Radarr and Sonarr.

In Prowlarr, use an app profile with RSS, automatic search, and interactive search enabled for indexers synchronized to Radarr and Sonarr. The built-in Standard profile enables all three. Run Settings > Apps > Sync App Indexers after changing the profile.

Import a torrent through Radarr or Sonarr, then check the link count:

stat -c '%h %n' /mnt/unas/arr_data/torrents/movies/<file> /mnt/unas/arr_data/media/movies/<file>

Both paths must report the same device and inode, and each must have a link count of at least 2. A higher count is valid when more hardlinks exist.

stat -c 'device=%d inode=%i links=%h path=%n' \
  /mnt/unas/arr_data/torrents/movies/<file> \
  /mnt/unas/arr_data/media/movies/<file>

Usenet imports do not need hardlinks because SABnzbd does not seed completed downloads. Radarr and Sonarr should atomically move completed files from /data/usenet/complete/movies or /data/usenet/complete/tv into the media library. The completed Usenet and media directories must report the same device:

stat -c 'device=%d path=%n' \
  /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

Configure matching SABnzbd categories:

movies → /data/usenet/complete/movies
tv     → /data/usenet/complete/tv

9. Set a Static IP

The LXC network is configured in its Proxmox config file, not inside the container. To assign a static IP, stop the LXC, edit the config, and restart.

VMID=101
pct stop "$VMID"

Open /etc/pve/lxc/${VMID}.conf and change the net0 line:

# Before (DHCP)
net0: name=eth0,bridge=vmbr0,hwaddr=...,ip=dhcp,type=veth

# After (static)
net0: name=eth0,bridge=vmbr0,hwaddr=...,ip=10.0.10.20/24,gw=10.0.10.1,type=veth

Or use sed to patch it in place:

sed -i 's/ip=dhcp/ip=10.0.10.20\/24,gw=10.0.10.1/' /etc/pve/lxc/${VMID}.conf
pct start "$VMID"

Verify inside the LXC:

pct enter "$VMID"
ip addr show eth0 | grep 'inet '

Do not edit /etc/network/interfaces inside the LXC. Proxmox manages the network config in the .conf file and the container's own network configuration is overwritten on start.

10. Rename a Container (VMID Change)

Proxmox has no pct rename command. To move a running LXC from one VMID to another (e.g., 120 → 101), rename the underlying LVM volume and swap the config files.

First, find the volume group and LV name:

lvs | grep <old-vmid>
# e.g.: vm-120-disk-0  vm-lxc-vg  ...

Stop the LXC, rename the LV, copy and update the config, then start under the new VMID:

OLD=120
NEW=101
VG=vm-lxc-vg

pct stop "$OLD"
lvrename "$VG" "vm-${OLD}-disk-0" "vm-${NEW}-disk-0"
sed "s/vm-${OLD}-disk-0/vm-${NEW}-disk-0/" /etc/pve/lxc/${OLD}.conf > /etc/pve/lxc/${NEW}.conf
rm /etc/pve/lxc/${OLD}.conf
pct start "$NEW"

If the LXC has multiple disks, repeat the lvrename step for each one and update all references in the new .conf file before starting.