$title =

;

$content = [

OpenStreams — a self-hosted home theater, built from scratch

A home theater I built from scratch.

OpenStreams is my self-hosted streaming platform. Ask for a movie or a show, and it finds it, downloads it, files it away, and plays it in the browser — all from a server in my house.

From request to screen

Every step runs on its own. Nobody touches a file.

  1. 1

    Request

    Pick a title in the OpenStreams app.

    Jellyseerr
  2. 2

    Find

    Search sources for the best-quality match.

    Prowlarr
  3. 3

    Download

    Fetch it over a VPN with a kill switch.

    qBittorrent + Gluetun
  4. 4

    Organize

    Rename and file it into the library, no duplicate copies.

    Sonarr / Radarr
  5. 5

    Play

    Stream it in the browser, hardware-accelerated.

    Jellyfin

Two machines, two jobs

The heavy lifting — storage, downloads, video encoding — lives on a repurposed desktop. The web app runs on a stack of Raspberry Pis, because I wanted to learn Kubernetes the hard way.

The NAS

Stores the library and runs the media services in Docker Compose.

CPU
Intel i3-9100F
GPU
AMD RX 590, for VAAPI transcoding
Drives
4 × 2 TB WD Red, 1 × 6 TB Seagate Exos
Storage
mergerfs pool with SnapRAID parity
Controller
LSI 9211-8i in IT mode
Front door
Caddy, with Cloudflare DNS certificates

The Pi cluster

Runs the OpenStreams web app on Kubernetes.

Nodes
5 × Raspberry Pi 4, one control plane and four workers
Orchestration
k3s with Kustomize manifests
App
Next.js, built for both arm64 and amd64
Images
GitHub Container Registry
Access
Admin tools behind Cloudflare Access

Where it stands

The core works end to end. The next big push is making the whole thing installable by someone who isn’t me.

  1. Shipped

    Automated media pipeline

    Request, search, download, organize, and library updates, all hands-off.

  2. Shipped

    The OpenStreams app

    Sign in, browse the library, request new titles, pick episodes, and watch in the browser.

  3. Shipped

    Production deploy

    Multi-platform images, a k3s deployment, and HTTPS through Caddy and Cloudflare.

  4. Building now

    A one-command installer

    One config file describes your setup. A containerized Go installer detects your hardware, writes every service’s config, and checks its own work.

What it’s made of

Almost everything underneath is open source. OpenStreams ties it together so viewers only ever see one app.

JellyfinMedia server and video streaming
JellyseerrHandles requests for new movies and shows
Sonarr & RadarrTrack, fetch, and organize TV and movies
ProwlarrManages search sources in one place
qBittorrent + GluetunDownloads, locked to a VPN tunnel
Next.jsThe OpenStreams app itself
Docker Compose & k3sRun the services on the NAS and the cluster
Caddy & CloudflareHTTPS, DNS, and access control

Things I learned the hard way

Mount paths have to match exactly

If the downloader and the organizers see the library at different paths, files get silently copied instead of linked, and storage use doubles without a single error.

Pooled drives and new torrent clients don’t mix

Recent qBittorrent uses memory-mapped disk I/O, which breaks on FUSE filesystems like mergerfs. Forcing POSIX-style I/O fixed it.

Next.js public variables are baked in at build time

Setting them at runtime does nothing. They have to go in as build arguments.

Shell scripts don’t scale into an installer

They’re fine for installing Docker. Everything past that belongs in a real language, which is why the installer is moving to Go.

];