My homelab: one $300 NAS, a Raspberry Pi, and zero waste
Everything I self-host runs on a single ~$300 N100 NAS that sips about 10 watts: roughly 20 Docker containers, a full media pipeline, DNS-level ad-blocking, and remote access with zero open ports. Here's the full tour — and why every piece is where it is.
- Homelab
- Self-Hosting
- Docker
- Networking
The one-paragraph version
Everything I self-host runs on a single UGREEN NAS with an Intel N100, 8 GB of RAM, four SATA bays and two NVMe slots, sitting next to a Raspberry Pi that owns the smart home. The NAS runs ~20 Docker containers — a full media-acquisition-and-streaming pipeline, DNS-level ad-blocking, a reverse proxy, remote access, and disk health monitoring — and it does it on a chip that sips ~10 W. The Pi runs Home Assistant. The two talk over the LAN. The entire thing is reachable from my phone anywhere in the world, ad-free, behind friendly name.nas URLs, with no ports forwarded to the internet. This post is the full tour and, more importantly, why every piece is where it is.
The hardware, and why this hardware
| Box | Spec | Job |
|---|---|---|
| UGREEN NAS "Home-NAS" | Intel N100 (4c/4t), 8 GB RAM, Debian 12 (UGOS Pro) | Everything except the smart home |
Raspberry Pi (192.168.0.78) | HAOS | Home Assistant, Zigbee, MQTT |
Why the N100? It's the sweet spot of the moment. Four modern Alder-Lake-E cores are enough to run twenty containers, and — critically — it has Intel Quick Sync Video baked in. That iGPU is the single most important feature in the whole build, and I'll come back to it under Jellyfin. Idle power is in the ~10 W range, which matters when the thing runs 24/7/365. As I write this it's been up 15 days with a load average around 10 — and that "high" load is almost entirely one SABnzbd download unpacking, not the box struggling.
Why a separate Pi for the smart home? This is the most deliberate architectural decision in the build, so it gets its own section.
Decision #1: the smart home does NOT live on the NAS
It used to. On 2026-06-06 I migrated Home Assistant, the Mosquitto MQTT broker, Zigbee2MQTT, and the physical Zigbee USB coordinator off the NAS and onto a dedicated Raspberry Pi. Reasons:
- Uptime domains should match the job. The NAS is where I tinker — I update containers, rebuild storage, restart Docker. The smart home is infrastructure my family expects to just work: light switches, sensors, automations. Coupling "the lights stopped responding" to "I was updating Radarr" is a bad design. Splitting them means I can take the NAS down for maintenance and the house stays smart.
- The Zigbee coordinator wants to be undisturbed. A USB radio dongle passed through to a container on a busy multi-purpose box is fragile. On the Pi it has the machine to itself.
- RAM. 8 GB is tight (more on this below). Evicting HA's stack freed real memory for the media pipeline.
But I didn't want two separate worlds. So Home Assistant is still reachable at http://home.nas like every other service — the NAS's Caddy reverse-proxies that hostname across the LAN to the Pi (reverse_proxy 192.168.0.78:8123), and my dashboard has a Smart Home tile that health-checks the Pi. Verified live just now: HA answers HTTP 200 both directly and through the proxy.
The gotcha that cost me an afternoon, recorded so I never repeat it: HA returns 400 Bad Request behind a reverse proxy unless you tell it to trust the proxy. The fix is use_x_forwarded_for: true plus Caddy's macvlan IP 192.168.0.93/32 in trusted_proxies in the Pi's configuration.yaml. There's a second subtle one: the Pi's SSH add-on rebuilds its authorized_keys on every restart, so the only durable place for my NAS's access key is the add-on's Configuration screen — not the usual ~/.ssh/authorized_keys.
Decision #2: storage is tiered by speed, not lumped together
The NAS has six drives, and they are emphatically not one big pool. They're split by the job each storage type is good at:
3 × 4 TB SATA (Seagate) → mdadm RAID5 → btrfs /volume2 (~7.3 TB) → BULK MEDIA
1 × NVMe WD SN770 → btrfs /volume3 (~960 GB) → ALL app configs/appdata
1 × NVMe Samsung 980 → btrfs /volume4 (~960 GB) → SCRATCH (in-progress, transcodes)
1 × ~931 GB SATA Toshiba → RAID1 → ext4 /volume1 → OLD DATA, kept as safety netThe reasoning:
- Bulk media on spinning RAID5 (
/volume2, currently 3.4 TB used of 7.3 TB). Movies and TV are big, sequential, and rarely written. Cheap-per-TB spinning disks in RAID5 give one-disk fault tolerance and lots of room. You don't need NVMe to stream a movie. - App configs on fast NVMe (
/volume3). Sonarr's database, Jellyfin's metadata, every container's config — these are small, random-access, and latency-sensitive. SQLite on a spinning disk shared with media I/O is misery. On a dedicated NVMe they fly, and they're isolated from the media churn. - A second NVMe purely as scratch (
/volume4). This is a performance and longevity trick. SABnzbd's in-progress downloads (unpacking, repairing) and Jellyfin's live transcodes are write-heavy, disposable, and would otherwise hammer either the RAID array or the config NVMe. Giving them their own throwaway disk means the bulk array only ever sees the finished file, and the config NVMe never gets thrashed.
One key subtlety: SAB's completed downloads land on /volume2 (the same filesystem as the media libraries) so the *arr apps can hardlink imports instead of copying them. A hardlink is instant and uses zero extra space — the file exists in both Downloads/ and Movies/ simultaneously. The incomplete dir is the one on scratch NVMe. That split is intentional.
The Toshiba on /volume1 is the pre-migration original data, deliberately kept online as a baking-period safety net before I wipe it and swap in a 3 TB disk for extra bulk. Belt and braces.
Decision #3: everything is Docker, one project per concern
Every service is a Docker Compose project under /volume3/docker/<name>/. Mostly LinuxServer.io images, all standardized on PUID=1000 PGID=10 TZ=Europe/London. This is boring on purpose — boring is reproducible.
Here's everything running right now (verified live). The media pipeline — `arr-stack` project, one compose file:
| Service | Port | Role |
|---|---|---|
| Sonarr | 8989 | TV: what to get, quality, renaming |
| Radarr | 7878 | Movies: same |
| Lidarr | 8686 | Music |
| Prowlarr | 9696 | Indexer hub — feeds the above |
| Bazarr | 6767 | Subtitles |
| SABnzbd | 8080 | The actual Usenet downloader |
| LazyLibrarian | 5299 | Ebooks/audiobooks (Readarr's dead) |
| Jellyseerr | 5055 | The "request a movie" front door |
| Navidrome | 4533 | Music streaming |
| Kavita | 5000 | Ebook reader |
| Audiobookshelf | 13378 | Audiobook player |
Standalone infrastructure projects:
| Service | Role |
|---|---|
| Jellyfin (8899) | The media server itself |
| Caddy | Reverse proxy → *.nas URLs |
| AdGuard Home | Network DNS + ad-blocking |
| Tailscale | Zero-config remote access + subnet router |
| Scrutiny | SMART disk-health monitoring |
Homepage (dash.nas) | The single dashboard tying it together |
| Watchtower | Nightly auto-updates |
| session-manager | Drive Claude Code / Copilot from my phone |
| macvlan-shim | A networking band-aid (see below) |
Parked until needed: Immich (photos — RAM-hungry) and Duplicati. They're stopped, not deleted, because RAM is precious.
How the media pipeline actually flows
I (or my wife) ask for a movie in Jellyseerr → it tells Radarr → Radarr asks Prowlarr's indexers where to find it → hands the NZB to SABnzbd → SAB downloads to scratch NVMe, unpacks, drops the finished file on the bulk array → Radarr hardlinks it into the library and pings Jellyfin to scan → it appears on the TV. Bazarr quietly fetches subtitles. Nobody touches a terminal.
Decision #4: friendly names and no open ports
Three pieces make the network feel like a polished product instead of a pile of IP:port combos:
- AdGuard Home is the LAN's DNS server. It blocks ads and trackers for every device in the house at the DNS level (no per-device setup), and it rewrites
*.nas→192.168.0.93. - Caddy lives on its own LAN IP (
192.168.0.93, via macvlan) and turnshttp://jelly.nas,http://home.nas,http://dash.nasetc. into reverse proxies to the right container. No port numbers to remember. - Tailscale is the remote-access layer. It's a mesh VPN, so my phone, laptop, and PC are on a private network with the NAS no matter where they are — and I've forwarded zero ports to the public internet. The NAS advertises itself as a subnet router and an exit node. The tailnet currently has my two iPhones, a MacBook, a PC, and a Pixel on it.
The clever bit: because Tailscale uses split-DNS to point .nas at AdGuard, the friendly names and the ad-blocking follow me off the LAN. On cellular data, jelly.nas still resolves and ads are still blocked.
The one wart, honestly recorded: macvlan network isolation means the NAS host can't reach Caddy's .93 address directly. The fix is the macvlan-shim container — a tiny Alpine container that creates a 192.168.0.94 shim interface and routes .93 back to the host. It's a hack, but it's a documented, restart-surviving hack, and it's the reason remote .nas access works end-to-end. (Note to self that's now in the runbook: .93 ignores ICMP, so test it with curl, never ping.)
Squeezing every drop out of an 8 GB / 10 W box
This is the part I'm proudest of. Running this much on an N100 with 8 GB isn't an accident — it's a series of deliberate trades.
1. Quick Sync is the whole ballgame for Jellyfin. The N100's iGPU is passed into the Jellyfin container (/dev/dri/renderD128, verified live). That means video transcoding — re-encoding a 4K HDR file on the fly so it plays on a phone — happens on fixed-function silicon, not the CPU. A software transcode would peg all four cores and melt; QSV does the same job at a few watts and leaves the CPU free for everything else. With HDR tone-mapping handled in hardware too, one cheap chip serves the whole house. Without Quick Sync, this build does not exist — you'd need a much bigger, hotter, pricier machine.
2. Memory discipline, treated as a real constraint. 8 GB with swap genuinely in use (3 GB swapped right now) forces honesty. Here's where the RAM actually goes, from a live snapshot:
sabnzbd 380 MB (and 152% CPU — actively downloading as I write this)
jellyfin 158 MB (idle; QSV means streaming barely touches RAM)
jellyseerr 138 MB
radarr 107 MB
adguardhome 101 MB
tailscale 84 MB
sonarr 75 MB
...
audiobookshelf 5 MB
macvlan-shim 316 KBThe whole media pipeline at rest fits in a couple of gigs. The discipline: RAM-hungry services (Immich, Duplicati) stay parked and are started on demand, SAB's article cache is kept small, and anything that idles cheap (Audiobookshelf at 5 MB!) is left running. Nothing is running "just in case."
3. Tiered storage means each disk only does what it's fast at. Covered above, but it's a juice-squeezing move as much as a reliability one: the bulk array never gets thrashed by transcodes or unpacks, the config NVMe never competes with media streaming, and the scratch NVMe absorbs all the disposable write-heavy churn — extending the life of the drives that hold data I actually care about.
4. Hardlinks, not copies. The same-filesystem hardlink trick means a 40 GB movie doesn't briefly become 80 GB during import. Across a library this saves enormous space and wear.
5. Auto-pilot maintenance. Watchtower updates a safe allowlist of containers nightly at 04:00 and prunes old images, so disk doesn't slowly fill with stale layers. Scrutiny watches SMART on all six disks — important here because the 4 TB media drives are used drives with 3.6–5.2 years of power-on hours, so a RAID5 rebuild is a genuine risk I monitor for rather than discover.
6. The meta-move: the box manages itself. There's a session-manager web app and a host-native session daemon that let me launch Claude Code sessions from my phone. The runbooks for this entire homelab are written as structured "skills" the agent reads — so fixing a stuck container or adding a service is a phone-from-the-pub operation, not a sit-at-the-desk one. The homelab documents and operates itself.
The architecture in one diagram
The honest summary
This build is a thesis: you don't need a rack, you need the right ~$300 chip and a tiered design. One N100 with Quick Sync replaces a power-hungry transcoding server. Splitting the smart home onto a Pi keeps the lights on while I tinker. Tiering storage by speed makes spinning rust and NVMe each do what they're best at. Tailscale gives me the whole thing in my pocket without exposing a single port. And ruthless memory discipline keeps twenty services humming in 8 GB. Every watt and every gigabyte is accounted for — which is the entire point.