Skip to content

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  │
                    └─────────────────┘         └──────────┘
  1. Seerr — Users submit requests for movies or TV shows
  2. Sonarr / Radarr — Receives the request and searches for releases via Prowlarr
  3. Prowlarr — Queries configured indexers and returns results to Sonarr/Radarr
  4. Flaresolverr — Assists Prowlarr with indexer sites behind Cloudflare
  5. qBittorrent — Downloads the selected release to /media/Downloads
  6. Sonarr / Radarr — Detects the completed download, moves/renames the file to the library
  7. 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

  1. Add the service to stacks/arrstack/docker-compose.yml in the homelab-ansible repo
  2. Map config to /opt/mediaserver/<service>/config:/config
  3. If it needs media access, mount paths from /stash/rodneystash/ at the paths the app expects
  4. 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