homelab-blueprint/docker-compose-files/arr-stack/README.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

477 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.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](../../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=<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:
```dotenv
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:
```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://<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`:
```dotenv
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:
```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 <service>
```
## Backup
Back up `DOCKERCONFDIR`. Shared media and downloads live in `DOCKERSTORAGEDIR`.