A self-hosted media server stack with HLS adaptive-bitrate streaming. Deploy on any Linux host with Docker — a cloud VPS, a dedicated server, a NAS, a Raspberry Pi for testing, or a spare laptop. Designed to be cheap to run, polite to the network, and pleasant to use over a slow connection.
| Feature | Component |
|---|---|
| Catalog browse + request flow (the page family/friends use) | Seerr |
| Streaming UI + library scanner | Jellyfin |
| Ingestion orchestrator (staging → media, webhook API, HLS dispatch) | this repo's orchestrator/ (FastAPI / SQLite) |
| Admin UI (stack management, logs, settings) | this repo's admin-app/ (Next.js); admin.<DOMAIN> |
| TV / movie automation | Sonarr / Radarr |
| Indexer aggregation | Prowlarr |
| Subtitles | Bazarr |
| Live TV middleware (M3U / EPG / HDHomeRun emulator for Jellyfin) | Dispatcharr |
| Mobile / TV unified client (streaming + Live TV + requests) | Streamyfin (iOS / Android / tvOS / Android TV) + server-side plugin |
| BitTorrent client | qBittorrent (forced through ProtonVPN) |
| Reverse proxy + automatic HTTPS | Caddy |
| Cloudflare challenge solver (for indexer scraping) | Byparr |
| HLS adaptive-bitrate encoder (optional profile) | this repo's hls-encoder/ |
The headline feature is the HLS pipeline: every imported video is
transcoded once into a 3-variant H.264 ladder (1080p / 720p / 480p) plus
per-language AAC audio renditions, written next to the source as a
hidden bundle, and served from a public CDN subdomain. Jellyfin streams
.strm files that point at the CDN — no live transcoding, no GPU
required, smooth playback even from mobile networks.
- Architecture
- Requirements
- Quickstart
- Documentation — architecture, deployment, configuration, maintenance, and more
- Security model
Every app sits behind a single Caddy instance that terminates TLS and
reverse-proxies by subdomain. Seerr is the public entry point (catalog +
requests, Jellyfin SSO); Jellyfin streams the library; the orchestrator
(FastAPI + SQLite) drives ingestion from staging/ to media/, exposes the
REST API, and dispatches HLS encoding; the admin app (Next.js) is the
operational UI. Sonarr / Radarr / Prowlarr / Bazarr handle automation and
indexers, with qBittorrent egress forced through ProtonVPN.
The headline is the HLS pipeline: each imported video is transcoded once into a
3-variant H.264 ladder plus per-language AAC, written next to the source and
served from a public CDN subdomain, so Jellyfin streams .strm files with no
live transcoding.
See docs/architecture.md for the full service map, network topology, filesystem layout, orchestrator internals, and the ingestion and HLS pipeline.
- A Linux host you can run Docker on. Tested on Ubuntu 24.04 and Debian 12. Other distros work if Docker + Compose v2 are installed.
- 2 vCPU / 2 GB RAM minimum (everything except the encoder runs on this; the encoder will simply be slow). 4 vCPU / 8 GB RAM comfortable for a small library. 4c/8t / 16+ GB RAM recommended if you ingest 1080p/4K regularly (matches the reference deployment — see Cost reference).
- A public IPv4 (or v6 with AAAA records) for incoming HTTPS — Caddy needs port 80/443 reachable to obtain Let's Encrypt certificates.
-
A directory exposed inside containers as
/data(theMEDIA_DIRenv var). Layout:$MEDIA_DIR/torrents/{tv,movies}— qBittorrent download targets.$MEDIA_DIR/staging/{tv,movies}— Sonarr/Radarr root folders; the orchestrator watches here and runs its policy engine.$MEDIA_DIR/incoming/— scratch space used by the orchestrator for in-progress merges (mkvmerge temporary output).$MEDIA_DIR/media/{tv,movies}— promoted library files; Jellyfin scans these paths.
torrents/,staging/, andmedia/must live on the same filesystem so Sonarr / Radarr can hardlink imports instead of copying. -
A second directory
ENCODER_CACHE_DIRfor HLS scratch. Should be on fast local storage (NVMe ideal) — never network-mounted. ~100 GB is plenty unless you encode 4K+ regularly. -
Storage backends that work: local disk, NFS export, SMB/CIFS share (e.g. Synology, TrueNAS, Hetzner Storage Box), iSCSI, S3FS-fuse. The stack doesn't care; it only sees POSIX paths.
-
Optional: a separate off-site target for backups (Hetzner Storage Box works, any SFTP server does). Encrypted snapshots via restic — see Backup. A single SMB share that holds both media (
MEDIA_DIR) and backups is fine; the backup container talks SFTP, not CIFS, so the two paths stay isolated.
- A registered domain (any registrar). 10 A records will point at the host (table further down).
- A WireGuard VPN with port forwarding. The reference is ProtonVPN Plus (NAT-PMP). Mullvad, AirVPN, PrivateInternetAccess all work — the only requirement is forwarded ports for incoming peer connections.
- Optional: a managed residential / ISP proxy subscription (e.g. IPRoyal ISP, ~$2.40/mo for one static IP). Lets Prowlarr scrape IP/ASN-gated trackers from a residential IP, entirely server-side. Skip if you only use Usenet or trackers that don't gate on IP. See Residential proxy for indexer scraping.
- SSH key pair (
~/.ssh/id_ed25519). git,rsync, and Docker Compose v2 (for local syntax checks).
For the impatient, on a fresh Ubuntu/Debian host:
# 1. Bootstrap the OS (creates user, installs Docker, hardens SSH).
# Storage drivers: 'cifs' (SMB), 'nfs', or 'none' for local disk.
ssh root@<HOST-IP>
export USERNAME=admin SSH_PUBKEY="ssh-ed25519 AAAA..."
export STORAGE_DRIVER=none # or 'cifs' / 'nfs' with extras below
bash <(curl -fsSL https://raw.githubusercontent.com/<you>/mediateca/main/setup-server.sh)
exit
# 2. Push the stack.
ssh <USERNAME>@<HOST-IP> 'mkdir -p /opt/servarr'
rsync -av --exclude='.git' --exclude='.claude' \
./ <USERNAME>@<HOST-IP>:/opt/servarr/
# 3. Configure.
ssh <USERNAME>@<HOST-IP>
cd /opt/servarr
cp .env.template .env && vim .env # fill in DOMAIN, ProtonVPN keys, API tokens, etc.
# 3a. Generate the admin-app password hash and add it to .env.
# The sed pipeline doubles every '$' so docker compose passes the
# hash through verbatim instead of interpreting `$2a` as a variable.
docker run --rm caddy:2-alpine caddy hash-password --plaintext '<your-password>' \
| sed 's|[$]|$$|g; s|^|ADMIN_PASSWORD_HASH=|' >> .env
# 4. Start.
docker compose up -d
docker compose logs -f caddy # watch certs being obtained
# 5. Wait for Sonarr and Radarr to be healthy, then wire them to the orchestrator.
docker run --rm --network servarr_servarr \
--env-file .env \
-v "$PWD/scripts:/scripts:ro" \
python:3.12-slim \
sh -c "pip install httpx==0.27.2 -q && python /scripts/bootstrap-arr.py"
# 6. (Optional) Enable HLS encoding.
# First start the encoder profile (compose profile: hls), then toggle dispatch
# via the admin app Settings page or directly via the API.
COMPOSE_PROFILES=hls docker compose up -d
curl -X PUT https://orchestrator.<DOMAIN>/api/settings \
-H "Authorization: Bearer $ADMIN_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hls_enabled": true}'Then walk through service configuration once. See the documentation for the full deployment guide.
The full guides live in docs/; AGENTS.md is the source of truth
for contributors (stack, commands, conventions).
- Architecture — service map, network topology, filesystem layout, orchestrator + pipeline internals
- Deployment guide — provision a host, storage, DNS,
.env, start the stack - Service configuration — per-service setup
- Live TV via Dispatcharr
- Residential proxy for indexer scraping
- Maintenance — retention, backups, notifications, health checks
- Troubleshooting
- Provider notes and cost reference
- Each app has its own login, with 2FA where supported.
- HTTPS everywhere, certs issued and rotated by Caddy via Let's Encrypt.
- Host firewall (UFW) configured by
setup-server.sh; pair with a cloud-side firewall on your provider (Hetzner Cloud Firewall, GCP Cloud Firewall, AWS Security Group) for defense in depth. - SSH: key-only, root login disabled, fail2ban watching auth logs.
- Unattended security upgrades enabled by
setup-server.sh. - qBittorrent exits traffic only through ProtonVPN — no host-IP torrent peer announcements, no DMCA exposure for the host.
- Indexer scraping (small HTTP queries, no torrent payload) goes through the managed residential proxy, isolating residential IP exposure to metadata-only traffic.
- The HLS CDN at
hls.<DOMAIN>is public by design (anyone with the URL can fetch segments). For a personal stack of legally-obtained or public-domain content this is fine; if you need access control, swap Caddy'sfile_serverfor aforward_authto a small auth proxy. - Secrets live in
.env(gitignored) and never get baked into images. - Anti-indexing: Caddy imports a
(no_index)snippet in every site block. Two layers, on by default:/robots.txtis served inline asUser-agent: */Disallow: /for polite crawlers.X-Robots-Tag: noindex, nofollow, noarchive, noimageindex, nosnippetrides on every other response — covers crawlers that don't fetch robots.txt and indirect links from outside. Both are needed because robots.txt only governs path crawling, not the indexability of a URL reached via an external link.
MIT — see LICENSE (add one if you intend to share).
Contributions and issue reports welcome.