Arrstack¶
Docker VM hosting the arr stack media management services, running on proxfold.
VM details¶
| Property | Value |
|---|---|
| VM ID | 101 |
| Hostname | arrstack |
| IP | 192.168.1.252 |
| OS | Debian 12 (bookworm) |
| Memory | 8192 MB |
| Cores | 2 |
| Storage | 32G root disk (single ext4 partition, no LVM; 1G /swapfile since 2026-05-20 — grown from 24G + old /dev/sda5 swap dropped) |
| Docker | 29.x with overlay2 storage driver pinned via docker_daemon_config (see Phase 5C lessons) |
| Management | Dockhand (Git-backed Docker Compose) — controller runs locally on this host |
Services¶
| Service | Image | Purpose | Port |
|---|---|---|---|
| gluetun | qmcgaw/gluetun | VPN gateway for qBittorrent (ProtonVPN WireGuard; port-forwarding hooks set qBit's listen port) | — |
| gluetun-slskd | qmcgaw/gluetun | Second VPN gateway for slskd (separate ProtonVPN WireGuard session → independent forwarded port) — see slskd VPN integration for the why | — |
| qBittorrent | lscr.io/linuxserver/qbittorrent | Download client (routed via gluetun) | 8080 |
| slskd | slskd/slskd | Soulseek client (routed via gluetun-slskd) — indexer + download client for Lidarr via Tubifarry — see slskd service page | 5030 |
| Sonarr | linuxserver/sonarr | TV show management | 8989 |
| Radarr | linuxserver/radarr | Movie management | 7878 |
| Lidarr | ghcr.io/hotio/lidarr:pr-plugins | Music library management — plugins-branch image required for Tubifarry — see Lidarr service page | 8686 |
| Prowlarr | linuxserver/prowlarr | Indexer manager | 9696 |
| Seerr | ghcr.io/seerr-team/seerr | Request management | 5055 |
| Tautulli | linuxserver/tautulli | Plex analytics (points at Plex CT 100 :32400) | 8181 |
| Flaresolverr | ghcr.io/flaresolverr/flaresolverr | CAPTCHA solver for Prowlarr | 8191 |
| beets | lscr.io/linuxserver/beets | Music tagger / sanity-pass tool — on-demand, NOT in Lidarr import pipeline — see beets service page | 8337 |
| MediaBot | mediabot:latest (local build) | Discord media management bot | — |
| Dockhand | fnsys/dockhand | Compose-from-Git controller — pulls compose specs from homelab-ansible and dispatches docker compose up to local Docker and to remote Hawser agents on nginx + n8n |
(UI on :3000) |
| korrosync | szaffarano/korrosync | KOReader progress sync server (Kobo Clara BW ↔ XTEINK X4) — see korrosync service page | 3030 (→ container :3000) |
| Mealie | ghcr.io/mealie-recipes/mealie | Recipe manager + meal planner + shopping list — see Mealie service page | 9000 |
Note
qBittorrent uses network_mode: "service:gluetun"; slskd uses network_mode: "service:gluetun-slskd" — each routes through its own ProtonVPN session so each gets its own NAT-PMP-forwarded port. The LAN-facing port for qBittorrent (8080) is exposed on gluetun; the slskd UI port (5030) is exposed on gluetun-slskd. Background on why one gluetun isn't enough: slskd VPN integration.
Request and download pipeline¶
User request Indexer search Download Import & organise
┌──────────┐ request ┌──────────┐ search ┌──────────┐ torrent ┌──────────────────┐
│ Seerr │ ────────> │ Sonarr │ ────────> │ Prowlarr │ ───────> │ qBittorrent │
│ │ │ Radarr │ <──────── │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └────────┬─────────┘
│ │
│ move/rename completed file │
│ <─────────────────────────────────────────────┘
│
▼
┌─────────────────┐ ┌──────────┐
│ ZFS Storage │ ──────> │ Plex │
│ /media/Movies │ │ │
│ /media/TV Shows│ │ streams │
└─────────────────┘ └──────────┘
- Seerr — Users submit requests for movies or TV shows
- Sonarr / Radarr — Receives the request and searches for releases via Prowlarr
- Prowlarr — Queries configured indexers and returns results to Sonarr/Radarr
- Flaresolverr — Assists Prowlarr with indexer sites behind Cloudflare
- qBittorrent — Downloads the selected release to
/media/Downloads - Sonarr / Radarr — Detects the completed download, moves/renames the file to the library
- Plex — Detects the new file and makes it available for streaming
Music acquisition (separate pipeline)¶
Lidarr replaces Seerr/Sonarr/Radarr's roles; Tubifarry plugin replaces Prowlarr+Flaresolverr; slskd replaces qBittorrent — all in one shorter chain:
Lidarr (monitored artist)
└─ Tubifarry plugin → slskd search via Soulseek
└─ slskd downloads to /downloads (→ /stash/rodneystash/Downloads/music)
└─ Lidarr detects, imports → /media/Music/<Artist>/<Album>/
└─ Plex Connect notification → Plex re-scans Tunes
beets sits parallel to this chain — invoked on-demand via docker exec for tag cleanup, not in the import path. See Lidarr, slskd, beets, and the music acquisition bringup runbook for the full Phase 6D context.
Docker Compose¶
Services are deployed via Dockhand, which manages the Docker Compose stack from the homelab-ansible repo (stacks/arrstack/docker-compose.yml). App configs are stored under /opt/mediaserver/ on the arrstack VM. Secrets are in .env (not committed — see .env.example).
Inline snippet is a snapshot, live file is the source of truth
The compose block below is a readable snapshot of the deployed stack. The authoritative version lives at stacks/arrstack/docker-compose.yml — diverges from this snippet in details like the gluetun port-forward up/down hooks and the qBittorrent → gluetun service_healthy dependency. When in doubt, read the file.
networks:
arr:
name: arr
external: true
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=protonvpn
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${PROTONVPN_WIREGUARD_PRIVATE_KEY}
- SERVER_COUNTRIES=Australia
- VPN_PORT_FORWARDING=on
- VPN_PORT_FORWARDING_PROVIDER=protonvpn
- PORT_FORWARD_ONLY=on
- FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24
- TZ=Australia/Adelaide
ports:
- 8080:8080 # qBittorrent WebUI
- 6881:6881 # qBittorrent incoming
- 6881:6881/udp
volumes:
- /opt/mediaserver/gluetun:/gluetun
restart: unless-stopped
networks:
- arr
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
depends_on:
- gluetun
environment:
- PUID=0
- PGID=0
- TZ=Australia/Adelaide
- WEBUI_PORT=8080
volumes:
- /opt/mediaserver/qbittorrent/config:/config
- /stash/rodneystash/Downloads:/media/Downloads
restart: unless-stopped
prowlarr:
image: linuxserver/prowlarr:latest
container_name: prowlarr
environment:
- PUID=0
- PGID=0
- TZ=Australia/Adelaide
volumes:
- /opt/mediaserver/prowlarr:/config
ports:
- 9696:9696
restart: unless-stopped
networks:
- arr
radarr:
image: linuxserver/radarr:latest
container_name: radarr
environment:
- PUID=0
- PGID=0
- TZ=Australia/Adelaide
volumes:
- /opt/mediaserver/radarr:/config
- /stash/rodneystash/Movies:/media/Movies
- /stash/rodneystash/Downloads:/media/Downloads
ports:
- 7878:7878
restart: unless-stopped
networks:
- arr
sonarr:
image: linuxserver/sonarr:latest
container_name: sonarr
environment:
- PUID=0
- PGID=0
- TZ=Australia/Adelaide
volumes:
- /opt/mediaserver/sonarr:/config
- /stash/rodneystash/TV Shows:/media/TV Shows
- /stash/rodneystash/Downloads:/media/Downloads
ports:
- 8989:8989
restart: unless-stopped
networks:
- arr
seerr:
image: ghcr.io/seerr-team/seerr:latest
container_name: seerr
init: true
environment:
- TZ=Australia/Adelaide
- LOG_LEVEL=debug
volumes:
- /opt/mediaserver/overseerr/config:/app/config
ports:
- 5055:5055
restart: unless-stopped
networks:
- arr
flaresolverr:
image: ghcr.io/flaresolverr/flaresolverr:latest
container_name: flaresolverr
environment:
- LOG_LEVEL=info
- LOG_HTML=false
- CAPTCHA_SOLVER=none
- TZ=Australia/Adelaide
ports:
- 8191:8191
restart: unless-stopped
networks:
- arr
mediabot:
image: mediabot:latest
container_name: mediabot
command: python bot.py
working_dir: /app
environment:
- DISCORD_TOKEN=${MEDIABOT_DISCORD_TOKEN}
- SONARR_URL=http://sonarr:8989
- SONARR_API_KEY=${SONARR_API_KEY}
- RADARR_URL=http://radarr:7878
- RADARR_API_KEY=${RADARR_API_KEY}
- SEERR_URL=http://seerr:5055
- SEERR_API_KEY=${SEERR_API_KEY}
- ALERT_CHANNEL_ID=${MEDIABOT_ALERT_CHANNEL_ID}
- STUCK_THRESHOLD_MINUTES=30
- CHECK_INTERVAL_MINUTES=5
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
restart: unless-stopped
networks:
- arr
Volume mounts¶
| Container | Container Path | Host Path | Purpose |
|---|---|---|---|
| Sonarr | /config |
/opt/mediaserver/sonarr |
Config and database |
| Sonarr | /media/TV Shows |
/stash/rodneystash/TV Shows |
TV library (ZFS) |
| Sonarr | /media/Downloads |
/stash/rodneystash/Downloads |
Download import path |
| Radarr | /config |
/opt/mediaserver/radarr |
Config and database |
| Radarr | /media/Movies |
/stash/rodneystash/Movies |
Movie library (ZFS) |
| Radarr | /media/Downloads |
/stash/rodneystash/Downloads |
Download import path |
| qBittorrent | /config |
/opt/mediaserver/qbittorrent/config |
Config |
| qBittorrent | /media/Downloads |
/stash/rodneystash/Downloads |
Download destination |
| Prowlarr | /config |
/opt/mediaserver/prowlarr |
Config |
| Seerr | /app/config |
/opt/mediaserver/overseerr/config |
Config — host path is the legacy overseerr directory carried forward from the old name |
| Tautulli | /config |
/opt/mediaserver/tautulli/config |
Config + history database |
Permissions¶
All Linuxserver.io containers run with PUID=0 and PGID=0 (root). Files created on the ZFS pool are owned by root:root with 755/644 permissions. Plex (uid 999) has read access via world-readable permissions.
Adding a new service¶
- Add the service to
stacks/arrstack/docker-compose.ymlin the homelab-ansible repo - Map config to
/opt/mediaserver/<service>/config:/config - If it needs media access, mount paths from
/stash/rodneystash/at the paths the app expects - Push to GitHub — Dockhand will pick up the change and redeploy
Retired components¶
| Component | Purpose | Status |
|---|---|---|
| Portainer | Docker management UI | Sunsetted — replaced by Dockhand |
| Watchtower | Automatic image updates | Removed — Dockhand handles updates |
| Homepage | Dashboard | Removed from compose |
LXC 103 (stash) |
SMB bridge from ZFS to Docker | Destroyed April 2026 |
| NAS CIFS mount in Plex | Previous media source | systemd unit disabled |
rodneystash Docker volume |
CIFS mount to NAS | Removed |
/opt/mediaserver/data/ |
Original empty media directories | Unused, can be deleted |
| Plex Docker container | Was in compose, LXC is active | Removed from compose |
| Bazarr | Subtitle manager, not deployed | Removed from compose |
Related¶
- Storage — ZFS pool and NFS export details
- Plex — media streaming end of the pipeline
- Network overview — service ports and IPs
- Backup & Restore runbook