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
Request
Pick a title in the OpenStreams app.
Jellyseerr - 2
Find
Search sources for the best-quality match.
Prowlarr - 3
Download
Fetch it over a VPN with a kill switch.
qBittorrent + Gluetun - 4
Organize
Rename and file it into the library, no duplicate copies.
Sonarr / Radarr - 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.
- Shipped
Automated media pipeline
Request, search, download, organize, and library updates, all hands-off.
- Shipped
The OpenStreams app
Sign in, browse the library, request new titles, pick episodes, and watch in the browser.
- Shipped
Production deploy
Multi-platform images, a k3s deployment, and HTTPS through Caddy and Cloudflare.
- 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.
- Planned
Watch-together rooms
Synced play, pause, and seek for friends watching from different places.
- Planned
Rooms you join with a code
Share a short code and a guest can watch along without making an account.
- Planned
In-room chat
Text chat alongside the video, with host moderation.
What it’s made of
Almost everything underneath is open source. OpenStreams ties it together so viewers only ever see one app.
| Jellyfin | Media server and video streaming |
|---|---|
| Jellyseerr | Handles requests for new movies and shows |
| Sonarr & Radarr | Track, fetch, and organize TV and movies |
| Prowlarr | Manages search sources in one place |
| qBittorrent + Gluetun | Downloads, locked to a VPN tunnel |
| Next.js | The OpenStreams app itself |
| Docker Compose & k3s | Run the services on the NAS and the cluster |
| Caddy & Cloudflare | HTTPS, 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.