My Home Media Server: A Raspberry Pi 4 for Compute, a Synology NAS for Storage
My home media server is a Raspberry Pi 4 named uther. It runs Plex, the entire *arr stack, qBittorrent, a request manager, a reverse proxy, Vaultwarden as a password manager, and this very blog — all in Docker Compose. It sips around five watts at idle, more like seven during peak indexing, and the most expensive part of the whole setup is a separate Synology NAS sitting on the same shelf doing nothing but being a quiet, reliable hard drive over NFS.
This is the honest tour. Architecture, wiring, the small decisions that make the difference between runs fine and runs for years untouched.
The architecture in one diagram
flowchart TD
NAS[("Synology NAS
3.6 TB · NFSv4")]
subgraph UTHER ["uther — Raspberry Pi 4 · Debian 13"]
direction TB
MEDIA["Media
Plex · Radarr · Sonarr · Prowlarr
Bazarr · Ombi · qBittorrent"]
WEB["Edge
nginx-proxy-manager · cloudflared"]
APPS["Apps
vaultwarden · danilo (this blog)"]
end
NET(["Public Internet"])
NAS -- "NFS · fstab" --> UTHER
UTHER -- "Cloudflare Tunnel" --> NET
Two boxes. One does compute, the other does storage. They never get confused about which is which.
Why split compute and storage
The lazy default for a home media setup is to put everything on the NAS. Synology’s “Container Manager” runs Docker, Plex has a native package, the *arr apps have community Synology packages. It works. But:
- The NAS is the part you want to be boring. Storage devices live longer when they do one thing — read and write blocks. Every extra service running on the NAS is another reason to reboot it, another process competing for IO, another security surface.
- The Pi is the part you want to be cheap. Five watts of compute for a media server that mostly sits there waiting for new releases is exactly the right amount of compute. A second-hand Pi 4 4GB is $40 in 2026.
- Replacing either one is easy. If the Pi dies, I buy another Pi,
restore the compose files and container configs from backup,
apt install docker.io, and we’re back. If the Synology dies, the Pi notices that NFS is gone and the containers gracefully stop. Both halves are independently replaceable.
Coupling everything to one device is the homelab equivalent of putting all your services on one big Kubernetes cluster — it works until the day it doesn’t.
The storage: NFS in /etc/fstab, not Docker volumes
The Pi sees the Synology as two NFS mounts that come up at boot. The
relevant lines in /etc/fstab:
nas:/volume1/qBittorrent /qbittorrent nfs defaults 0 0
nas:/volume1/Plex /plex nfs defaults 0 0
That’s it. No Docker volume drivers, no per-container NFS configuration,
no third-party CSI nonsense. The OS handles the mounts, the containers
just see /plex/Movies, /plex/TV, and /qbittorrent as ordinary host
directories — bind-mounted in:
volumes:
- /radarr/data:/config
- /plex/Movies:/movies
- /qbittorrent:/downloads
If NFS hiccups for a moment, every container that’s mid-write notices and recovers when the mount comes back. The kernel’s NFS client is the right layer for this. Pushing it into Docker would be re-inventing what the OS already does.
The *arr stack, end to end
For anyone new to this corner of self-hosting, the *arr stack is a small ecosystem of open-source apps that automate the boring parts of a media library:
| Component | Role |
|---|---|
| Prowlarr | Indexer manager — talks to public/private trackers, hands results to Radarr and Sonarr |
| Radarr | Movies — adds to your watchlist, finds the right release, hands it to qBittorrent, renames/moves it on completion |
| Sonarr | The same as Radarr but for TV series, episode-aware |
| qBittorrent | The actual download client |
| Bazarr | Subtitles — watches your library and fetches matching subs in your preferred languages |
| Ombi | Request frontend — non-technical users ask for content; Ombi pushes the request into Sonarr/Radarr |
| Plex | The media server — indexes the finished library and streams it to clients |
The loop, in one sentence: Ombi receives a request → Sonarr/Radarr ask Prowlarr where to find it → qBittorrent downloads to the Synology over NFS → Sonarr/Radarr rename and move the file to the Plex folder (still on the Synology) → Plex notices it → Bazarr finds subtitles.
The whole thing runs on a Raspberry Pi because none of those steps need real CPU. Prowlarr is glorified HTTP. Radarr/Sonarr are bookkeeping apps. qBittorrent is bandwidth-bound, and the Pi’s gigabit Ethernet is the bottleneck, not its CPU. Plex on the Pi handles direct play — where the client device decodes the file natively and the server just streams bytes. That’s 95% of my playback.
The remaining 5% — where a phone wants a transcoded version of a 4K
HEVC file on cellular — Plex politely tells the client “this device
doesn’t support that format” and the client (or I) pick a different
quality. Hardware transcoding on the Pi 4 is technically possible via
/dev/dri but it’s not the right tool for that job, and I’ve never
missed it.
linuxserver.io images everywhere
Every container in this stack uses an image from linuxserver.io. The reasons matter:
- First-class ARM64 multi-arch builds. Every linuxserver image
ships native
arm64next toamd64. No emulation, no QEMU, no performance penalty on a Pi. - Consistent
PUID/PGIDenvironment variables across every image. One conventions to learn, applied uniformly. - Sane defaults for paths and volumes. Their docs all assume
/configfor app state and bind-mount targets for media — exactly how I have it wired. - A predictable update cadence. New upstream releases land in the image within days, not weeks.
A representative compose file from this stack — the Radarr one — is ten lines and contains nothing surprising:
services:
radarr:
image: lscr.io/linuxserver/radarr:latest
container_name: radarr
network_mode: host
environment:
- PUID=0
- PGID=0
- TZ=America/Sao_Paulo
volumes:
- /radarr/data:/config
- /plex/Movies:/movies
- /qbittorrent:/downloads
restart: unless-stopped
Sonarr, Prowlarr, Bazarr and Plex follow the same shape. One service
per directory, one compose file per service. No giant 200-line monolith,
no depends_on: chains, no service mesh. The containers find each
other on localhost because they all use network_mode: host.
External access without opening ports
This is the part most home-server tutorials get wrong. They tell you to forward 32400 on your router to the Pi for Plex remote access, maybe 80/443 for the reverse proxy. Then they tell you to configure fail2ban because you just exposed your home network to the internet.
I do none of that. There are zero inbound ports open on my home
router. External access happens through a Cloudflare Tunnel: a
small container (cloudflare/cloudflared) maintains an outbound
connection to Cloudflare’s edge, and Cloudflare routes incoming
requests through that tunnel back to the Pi.
Internally, an nginx-proxy-manager container handles the TLS
termination and routing to each backend. Plex on :32400, this blog on
:1313, Vaultwarden on its port, all multiplexed behind sensible
hostnames.
The combination — Cloudflare Tunnel outward, nginx-proxy-manager inward — gives me HTTPS everywhere, no port forwards, no certificates to renew manually, and Cloudflare’s network in front of every request. For a home media server in 2026, this is the only sane way to expose anything.
Plex in 2026 — and why I’m still on it
I can’t write this post without addressing the elephant in the room: Plex’s 2026 paywall changes. As of March 2026, remote streaming is no longer free for the server owner or the viewer. You need either:
- A Plex Pass (server owner): $6.99/month, $69.99/year, or $249.99 lifetime — going to $749.99 on July 1, 2026.
- A Remote Watch Pass (viewers): $1.99/month, going to $2.99/month on June 1.
Reasonable people are upset, and the timing of the lifetime price hike feels like a slow-motion farewell to the free tier. Jellyfin is right there — free, open-source, no telemetry, no cloud auth, no paywall.
I’m staying on Plex, with eyes open:
- I have a Plex Pass from years ago, before the changes.
- The non-technical family members who use my server know the Plex apps on their TVs. “Switch to Jellyfin” is a non-trivial conversation multiplied by every TV in every household.
- Plex’s apps on Roku, Apple TV, Samsung, LG, and game consoles remain more polished than Jellyfin’s. That gap is closing — and I’ll happily reevaluate when it closes — but in May 2026 it’s still real.
If I were starting from zero today, I’d give Jellyfin a much harder look. But this stack has been running for years and the cost of switching horses is higher than the cost of one already-paid Plex Pass. Honesty matters more than purity here.
Updates, by script — and manually
I deliberately don’t run any auto-update mechanism. Containers updating themselves while I sleep is a recipe for “why doesn’t Plex work this morning.”
What I do have is a small shell script that pulls the latest images and
recycles each compose project in turn. It’s a script, not a cron job —
I run it by hand, at the keyboard, watching it happen. If anything
breaks I know exactly when, and the cure is docker compose up -d
with the previous tag. It takes a few minutes once every couple of
weeks. That’s the right cadence for a home server: scripted enough to
be repeatable, manual enough to stay safe.
The honest bottom line
A Raspberry Pi 4 has no business running a 12-container production-feeling stack with multiple users, external access, automated media management and a personal blog on top. Except that it does, and it has been, and it costs less than a fast-food meal per month in electricity.
The trick is giving each piece exactly one job: the Synology stores bytes, the Pi orchestrates services, NFS bridges them, Cloudflare Tunnel handles ingress, nginx-proxy-manager handles routing, and the *arr apps automate the parts that are too repetitive to do by hand.
Nothing here is clever. It’s just a small number of well-chosen pieces, wired in the most obvious way they can be wired. The home server ecosystem in 2026 makes this possible in an afternoon. It’s a good time to be running your own media stack.