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

679 lines
18 KiB
YAML

# arr-stack for Docker in an unprivileged Proxmox LXC.
# DOCKERCONFDIR must be local LXC disk. DOCKERSTORAGEDIR must be the NFS-backed
# Proxmox bind mount. Service-specific mounts follow the TRaSH Guides layout.
# Full shared root for storage validation and Starr apps.
x-arr_data-mount: &arr_data-mount
type: bind
source: {{DOCKERSTORAGEDIR}}
target: /data
bind:
create_host_path: false
# Download clients and media consumers receive only the subtrees they need.
x-torrents-data-mount: &torrents-data-mount
type: bind
source: {{DOCKERSTORAGEDIR}}/torrents
target: /data/torrents
bind:
create_host_path: false
x-usenet-data-mount: &usenet-data-mount
type: bind
source: {{DOCKERSTORAGEDIR}}/usenet
target: /data/usenet
bind:
create_host_path: false
x-media-data-mount: &media-data-mount
type: bind
source: {{DOCKERSTORAGEDIR}}/media
target: /data/media
bind:
create_host_path: false
services:
storage-guard:
image: alpine:3.20
container_name: arr-storage-guard
restart: "no"
network_mode: none
labels:
- "com.synology.stack=media"
- "com.synology.service=storage-guard"
user: "{{PUID}}:{{PGID}}"
volumes:
- *arr_data-mount
command:
- /bin/sh
- -ec
- |
marker="/data/{{ARR_DATA_MARKER:-.arr-stack-storage}}"
test -f "$$marker" || {
echo "ERROR: required storage marker is missing: $$marker" >&2
echo "Refusing to start data-consuming services." >&2
exit 1
}
test_file="/data/.arr-stack-write-test-$$$$"
touch "$$test_file"
rm "$$test_file"
echo "Shared data mount marker and write access verified."
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
hostname: gluetun
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=vpn"
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "8080:8080" # qbittorrent WebUI
- "8000:8000/tcp" # Gluetun WebUI
networks:
- vpn-network
volumes:
- {{DOCKERCONFDIR}}/gluetun:/gluetun
environment:
VPN_SERVICE_PROVIDER: {{VPN_SERVICE_PROVIDER:-airvpn}}
VPN_TYPE: wireguard
WIREGUARD_PRIVATE_KEY: {{WIREGUARD_PRIVATE_KEY}}
WIREGUARD_PRESHARED_KEY: {{WIREGUARD_PRESHARED_KEY}}
WIREGUARD_ADDRESSES: {{WIREGUARD_ADDRESSES}}
FIREWALL_VPN_INPUT_PORTS: {{FIREWALL_VPN_INPUT_PORTS}}
SERVER_COUNTRIES: {{SERVER_COUNTRIES:-}}
WIREGUARD_PUBLIC_KEY: {{WIREGUARD_PUBLIC_KEY:-}}
WIREGUARD_ENDPOINT_IP: {{WIREGUARD_ENDPOINT_IP:-}}
WIREGUARD_ENDPOINT_PORT: {{WIREGUARD_ENDPOINT_PORT:-}}
UPDATER_PERIOD: 24h
TZ: {{TZ}}
healthcheck:
test: ["CMD-SHELL", "ping -c 1 1.1.1.1 || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
qbittorrent:
image: ghcr.io/hotio/qbittorrent:latest
container_name: qbittorrent
# Use direct container network mode for better compatibility with single-service recreate flows.
network_mode: "container:gluetun"
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=torrent-client"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
WEBUI_PORTS: 8080/tcp,8080/udp
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/qbittorrent:/config
- *torrents-data-mount
depends_on:
gluetun:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
deploy:
resources:
limits:
# 1G is insufficient for large torrents (20 GB+): piece-verification buffers
# can push usage past 1 GiB and trigger the cgroup OOM killer mid-download.
memory: 3G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
flaresolverr:
image: flaresolverr/flaresolverr:latest
container_name: flaresolverr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=captcha-solver"
networks:
- vpn-network
ports:
- "8191:8191"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
LOG_LEVEL: info
CAPTCHA_SOLVER: none
MAX_TIMEOUT: 60000
BROWSER_TIMEOUT: 40000
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8191/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
sabnzbd:
image: ghcr.io/hotio/sabnzbd:latest
container_name: sabnzbd
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=usenet-client"
networks:
- vpn-network
ports:
- "8282:8282"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
WEBUI_PORTS: 8282/tcp,8282/udp
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/sabnzbd:/config
- *usenet-data-mount
depends_on:
storage-guard:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8282/api?mode=version || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
deploy:
resources:
limits:
memory: 2G
reservations:
memory: 512M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
prowlarr:
image: ghcr.io/hotio/prowlarr:latest
container_name: prowlarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=indexer-manager"
networks:
- vpn-network
ports:
- "9696:9696"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/prowlarr:/config:rw
depends_on:
flaresolverr:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9696/ping || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
# 198 MiB at idle leaves only ~314 MiB headroom at 512M; RSS syncs
# across many indexers can exhaust that during a search flood.
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
radarr:
image: ghcr.io/hotio/radarr:latest
container_name: radarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=movie-pvr"
networks:
- vpn-network
ports:
- "7878:7878"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/radarr:/config
- *arr_data-mount
depends_on:
prowlarr:
condition: service_healthy
gluetun:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:7878/ping || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
sonarr:
image: ghcr.io/hotio/sonarr:latest
container_name: sonarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=tv-pvr"
networks:
- vpn-network
ports:
- "8989:8989"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/sonarr:/config
- *arr_data-mount
depends_on:
prowlarr:
condition: service_healthy
gluetun:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8989/ping || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
bazarr:
image: ghcr.io/hotio/bazarr:latest
container_name: bazarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=subtitle-manager"
networks:
- vpn-network
ports:
- "6767:6767"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
UMASK: 002
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/bazarr:/config
- *media-data-mount
depends_on:
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
healthcheck:
test: ["CMD-SHELL", "wget --quiet --tries=1 --spider http://localhost:6767/ || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 90s
deploy:
resources:
limits:
# 512M OOMs on subtitle batch events for new 25 GB 4K imports (provider
# queries + mediainfo extraction + subtitle parsing can spike 200-300 MiB
# on top of the 345 MiB observed at idle).
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
emby:
image: lscr.io/linuxserver/emby:latest
container_name: emby
restart: unless-stopped
runtime: nvidia
labels:
- "com.synology.stack=media"
- "com.synology.service=media-server"
networks:
- vpn-network
ports:
- "8096:8096"
environment:
PUID: {{PUID}}
PGID: {{PGID}}
TZ: {{TZ}}
UMASK_SET: 022
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/emby:/config:rw
- *media-data-mount
healthcheck:
test: ["CMD-SHELL", "timeout 1 bash -c '</dev/tcp/localhost/8096' || exit 1"]
interval: 30s
timeout: 5s
retries: 5
start_period: 180s
depends_on:
storage-guard:
condition: service_completed_successfully
deploy:
resources:
limits:
memory: 3G
reservations:
memory: 1G
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
seerr:
image: ghcr.io/seerr-team/seerr:latest
container_name: seerr
init: true
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=media-requests"
networks:
- vpn-network
ports:
- "5055:5055"
environment:
TZ: {{TZ}}
PORT: 5055
LOG_LEVEL: info
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/seerr:/app/config
depends_on:
emby:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/status || exit 1"]
interval: 15s
timeout: 3s
retries: 3
start_period: 20s
deploy:
resources:
limits:
# 216 MiB at idle; full Emby library sync on startup can spike to
# 400-450 MiB, leaving only ~60 MiB headroom at 512M.
memory: 1G
reservations:
memory: 256M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
recyclarr:
image: ghcr.io/recyclarr/recyclarr:8.6.0
container_name: recyclarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=arr-config-manager"
networks:
- vpn-network
user: "{{PUID}}:{{PGID}}"
environment:
TZ: {{TZ}}
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/recyclarr:/config
depends_on:
radarr:
condition: service_healthy
sonarr:
condition: service_healthy
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 64M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
unpackerr:
image: ghcr.io/unpackerr/unpackerr:latest
container_name: unpackerr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=archive-extractor"
networks:
- vpn-network
user: "{{PUID}}:{{PGID}}"
environment:
TZ: {{TZ}}
UN_LOG_FILE: /config/unpackerr.log
UN_SONARR_0_URL: http://sonarr:8989
UN_SONARR_0_API_KEY: {{SONARR_API_KEY}}
UN_SONARR_0_PATHS_0: /data
UN_SONARR_0_PROTOCOLS: torrent,TorrentDownloadProtocol,usenet,UsenetDownloadProtocol
UN_RADARR_0_URL: http://radarr:7878
UN_RADARR_0_API_KEY: {{RADARR_API_KEY}}
UN_RADARR_0_PATHS_0: /data
UN_RADARR_0_PROTOCOLS: torrent,TorrentDownloadProtocol,usenet,UsenetDownloadProtocol
volumes:
- /etc/localtime:/etc/localtime:ro
- {{DOCKERCONFDIR}}/unpackerr:/config
- *torrents-data-mount
- *usenet-data-mount
depends_on:
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
sabnzbd:
condition: service_healthy
gluetun:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
decluttarr:
image: ghcr.io/manimatter/decluttarr:latest
container_name: decluttarr
restart: unless-stopped
labels:
- "com.synology.stack=media"
- "com.synology.service=arr-queue-cleaner"
networks:
- vpn-network
environment:
TZ: {{TZ}}
PUID: {{PUID}}
PGID: {{PGID}}
LOG_LEVEL: {{DECLUTTARR_LOG_LEVEL:-INFO}}
TEST_RUN: {{DECLUTTARR_TEST_RUN:-False}}
TIMER: {{DECLUTTARR_TIMER:-10}}
REMOVE_FAILED_DOWNLOADS: "True"
REMOVE_FAILED_IMPORTS: "True"
REMOVE_METADATA_MISSING: "True"
REMOVE_MISSING_FILES: "True"
REMOVE_ORPHANS: "True"
REMOVE_STALLED: |
max_strikes: {{DECLUTTARR_MAX_STRIKES:-3}}
DETECT_DELETIONS: {{DECLUTTARR_DETECT_DELETIONS:-True}}
SONARR: >
- base_url: "http://sonarr:8989"
api_key: "{{SONARR_API_KEY}}"
RADARR: >
- base_url: "http://radarr:7878"
api_key: "{{RADARR_API_KEY}}"
QBITTORRENT: >
- base_url: "{{QBITTORRENT_BASE_URL:-http://gluetun:8080}}"
username: "{{QBITTORRENT_USERNAME:-}}"
password: "{{QBITTORRENT_PASSWORD:-}}"
name: "qBittorrent"
SABNZBD: >
- base_url: "http://sabnzbd:8282"
api_key: "{{SABNZBD_API_KEY:-}}"
name: "SABnzbd"
volumes:
- /etc/localtime:/etc/localtime:ro
- *media-data-mount
depends_on:
sonarr:
condition: service_healthy
radarr:
condition: service_healthy
sabnzbd:
condition: service_healthy
gluetun:
condition: service_healthy
storage-guard:
condition: service_completed_successfully
deploy:
resources:
limits:
memory: 512M
reservations:
memory: 128M
security_opt:
- no-new-privileges:true
logging:
driver: json-file
options:
max-file: "{{DOCKERLOGGING_MAXFILE}}"
max-size: "{{DOCKERLOGGING_MAXSIZE}}"
networks:
vpn-network:
driver: bridge
name: vpn_network
labels:
- "com.synology.network=media"