- 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
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:
- Open the UniFi Drive web interface.
- Go to Settings → Services.
- 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:
- Go to Shares and click
arr_data. - Under Sharing Protocols, enable NFS.
- 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. - 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,
chownsilently fails and containers cannot write. - 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_datashare 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.datadirectory and cannotchownanything 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.
8. Verify Hardlinks
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/interfacesinside the LXC. Proxmox manages the network config in the.conffile 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.