No description
Find a file
Kevin 6edd353679 Correct networking claims: mediastack and mediaserver are separate hosts
Bridge networks are host-local, so the two stacks don't actually share
one; Unpackerr's hardcoded host IP is the correct cross-host approach,
not a workaround pending removal.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 12:05:19 +02:00
README.md Correct networking claims: mediastack and mediaserver are separate hosts 2026-07-15 12:05:19 +02:00

arrframework

Overview and combined reference for the self-hosted media automation stack. This repo doesn't run anything itself — it documents how mediaserver, mediastack, and recyclarr fit together, and holds a snapshot of their configs.

The three repos

Repo Role
mediastack Core *arr stack: indexers, download automation, requests, dashboard
mediaserver Media playback + post-processing: transcoding, streaming, download client, monitoring
recyclarr Config-as-code for Sonarr/Radarr quality profiles & custom formats (TRaSH Guides sync)

mediastack and recyclarr run together on one host (a container/LXC); mediaserver runs on a separate VM. Within a single host's compose stack, services share a bridge network (named mediastack) and can reach each other by container name (e.g. sonarr, prowlarr). Across hosts, though, bridge networks aren't shared — mediaserver's services reach mediastack's services over the host network via IP address (e.g. Unpackerr's 10.10.88.11 URLs), not by container name.

Architecture / request flow

Seerr (5055)  ──requests──▶  Sonarr (8989) / Radarr (7878) / Lidarr (8686)
                                   │  metadata & search
                                   ▼
                             Prowlarr (9696) ──indexers──▶ FlareSolverr (8191, cf-bypass)
                                   │  sends grab to
                                   ▼
                             SABnzbd (8080)  ──downloads land in /mnt/samba/downloads
                                   │
                                   ▼
                             Unpackerr  (extracts archives, notifies *arr apps)
                                   │
                                   ▼
                     Sonarr/Radarr import ──▶ /mnt/unifipronas/{movies,series,anime,music}
                                   │
                                   ▼
                             Bazarr (6767, subtitles)
                                   │
                                   ▼
                             Tdarr (8265, transcode/optimize on library)
                                   │
                                   ▼
                             Jellyfin (8096, host network, plays final files)

Homarr (7575)        — dashboard over everything
Recyclarr             — periodically pushes TRaSH quality profiles/custom formats into Sonarr & Radarr
Watchtower             — auto-updates containers nightly (04:00), disabled via label on *arr apps
node-exporter (9100)  — host metrics for Prometheus, present in both stacks

mediastack — indexers, *arr apps, requests, dashboard

File: mediastack/docker-compose.yml, env: mediastack/.env.example

Services (all on the mediastack bridge network, config in /mnt/appdata/<service>):

  • homarr (:7575) — dashboard, reads docker.sock (ro) to show container status.
  • flaresolverr (:8191, localhost-only) — Cloudflare bypass proxy used by Prowlarr.
  • prowlarr (:9696) — indexer manager, feeds Sonarr/Radarr/Lidarr. Depends on flaresolverr being healthy.
  • sonarr (:8989) — TV/anime management. Media at /mnt/unifipronas/{series,anime}, downloads at /mnt/samba/downloads.
  • radarr (:7878) — movie management. Media at /mnt/unifipronas/movies.
  • lidarr (:8686) — music management. Media at /mnt/unifipronas/music.
  • bazarr (:6767) — subtitle fetching for Sonarr/Radarr libraries.
  • seerr (:5055) — request UI (Jellyseerr/Overseerr-style), talks to Sonarr/Radarr.
  • recyclarr — no ports, config synced from /mnt/appdata/recyclarr; see the recyclarr section below.
  • watchtower — auto-updates images nightly, com.centurylinklabs.watchtower.enable=false label opts the *arr apps out (so recyclarr-managed config doesn't get clobbered by surprise upgrades).
  • node-exporter (:9100) — Prometheus host metrics.

Env vars needed (.env, copy from .env.example):

PUID=1000
PGID=1000
TZ=Etc/UTC
HOMARR_SECRET_KEY=      # random secret for homarr encryption
SONARR_API_KEY=         # from Sonarr → Settings → General, also used by recyclarr
RADARR_API_KEY=         # from Radarr → Settings → General, also used by recyclarr

Bring up: docker compose up -d from mediastack/.

mediaserver — transcoding, streaming, download client, monitoring

File: mediaserver/docker-compose.yml, env: mediaserver/.env.example

  • tdarr_master (:8265, web UI/API :8266) — transcode scheduling. Uses Intel/AMD GPU passthrough (/dev/dri) for hardware transcode (ffmpegVersion=7, iHD driver).
  • tdarr_node — worker attached to tdarr_master over the mediastack network; also has GPU access. Waits for master to be healthy.
  • sabnzbd (:8080) — Usenet download client; downloads land in /mnt/samba/downloads.
  • unpackerr — watches SABnzbd's completed downloads, extracts archives, and notifies Sonarr/Radarr/Lidarr to import. Talks to the *arr apps by host IP (10.10.88.11), not container name — this is the correct approach here, since the *arr apps run on a different host (the mediastack container/LXC) and Docker bridge networks don't span hosts. Switching to container-name DNS would require adding real cross-host networking (e.g. Tailscale, a macvlan/overlay network, or a reverse proxy).
  • jellyfin (:8096, network_mode: host) — media server/player, hardware-accelerated (iHD, /dev/dri, video/992 groups).
  • watchtower — same nightly auto-update pattern as in mediastack (separate instance, no cross-stack shared state).
  • node-exporter (:9100) — same as mediastack's; note running both stacks together would port-conflict on 9100 — only run one node-exporter per host.

Env vars needed (.env, copy from .env.example):

TZ=America/New_York
SONARR_API_KEY=
RADARR_API_KEY=
LIDARR_API_KEY=

Bring up: docker compose up -d from mediaserver/. There's a known TODO in the compose file: volume paths mix two conventions (/home/resixh/appdata/... per-app config vs /mnt/... shared media/cache) — intentionally left alone to avoid breaking running state; consolidate only during a planned maintenance window.

recyclarr — quality profile / custom format sync

Config-only repo (no compose file of its own — it runs as the recyclarr service inside mediastack's compose, image ghcr.io/recyclarr/recyclarr:8).

  • recyclarr.yml — main config. For each of sonarr.series and radarr.movies:
    • base_url (http://sonarr:8989, http://radarr:7878 — container-name DNS on the mediastack network) and api_key (via !env_var SONARR_API_KEY / RADARR_API_KEY).
    • quality_definition — min/preferred/max size thresholds per resolution/source tier (720p2160p, HDTV/WEBDL/WEBRip/Bluray).
    • quality_profiles — either pulled wholesale from TRaSH Guides via trash_id (e.g. "HD Series (EN+NL)", "HD Movies (EN+NL)", "4K Movies (EN+NL)"), or defined fully inline for custom needs (anime dual-audio/dub profiles with hand-tuned upgrade/quality_sort/qualities).
    • custom_formats — score assignments per profile, referencing formats by trash_id: real TRaSH guide hex IDs (commented with their human name, e.g. 418f50b10f1907201b6cfdf881f467b7 # Anime Dual Audio) or local IDs (cf-block-german, etc.) matching files under custom-formats/.
  • settings.yml — declares resource_providers pointing recyclarr at /config/custom-formats/{radarr,sonarr} inside the container so the local JSON custom formats get loaded alongside TRaSH's.
  • custom-formats/{radarr,sonarr}/*.json — locally-authored custom formats not in the official TRaSH guide: anime-dual-audio.json, block-{french,german,portuguese,spanish}.json. Each has trash_id, name, specifications (e.g. LanguageSpecification, ReleaseTitleSpecification). If a format should apply to both apps, matching files exist under both custom-formats/radarr/ and custom-formats/sonarr/.

What it actually does: on each run, recyclarr sync pushes the quality definitions, quality profiles, and custom-format scores from these YAML/JSON files into the live Sonarr/Radarr instances via their API — so profile tuning is version-controlled here instead of clicked through the web UI.

Validate without touching the live instances:

recyclarr config validate     # schema-check recyclarr.yml, no network needed
recyclarr sync --preview      # dry-run against Sonarr/Radarr, shows planned changes

Requires SONARR_API_KEY / RADARR_API_KEY env vars and network access to a running Sonarr/Radarr.

Putting it all together: deployment order

  1. mediastack: set .env, run docker compose up -d. Wait for Sonarr/Radarr to come up, grab their API keys from Settings → General.
  2. recyclarr: drop the API keys into the environment the recyclarr service in mediastack reads from (same .env), then either let its container run on schedule or docker compose run --rm recyclarr sync once to seed profiles/formats.
  3. mediaserver: set .env with the same Sonarr/Radarr/Lidarr keys (used by Unpackerr), run docker compose up -d on the mediaserver VM. Sonarr/Radarr/Lidarr run on the separate mediastack host, so Unpackerr reaches them by host IP (10.10.88.11) rather than container name — that's expected given the two-host topology, not a placeholder.
  4. Point Prowlarr (in mediastack) at your indexers, and Sonarr/Radarr/Lidarr at Prowlarr and at SABnzbd (in mediaserver) as the download client.
  5. Confirm both stacks' node-exporter isn't double-bound on :9100 — only run one.

Shared conventions across all three

  • PUID/PGID=1000, TZ set per-service via env.
  • Media library root: /mnt/unifipronas/{movies,series,anime,music} (mediastack) — note mediaserver's Tdarr/Jellyfin instead use /mnt/unifipronas/jelly/{movies,series,anime,music}, a different path for the same content; worth confirming these actually point at the same data or are two separate mount conventions before assuming parity.
  • Downloads land in /mnt/samba/downloads, shared between SABnzbd, Unpackerr, and the *arr apps.
  • Per-app config under /mnt/appdata/<service> (mediastack) or /home/resixh/appdata/<service> (mediaserver) — again two different conventions, not yet reconciled.
  • Watchtower auto-updates nightly at 04:00 in both stacks; *arr apps opt out via com.centurylinklabs.watchtower.enable=false so recyclarr-managed state isn't disrupted by a surprise version bump mid-cycle.