Why bother

I manage endpoints and identity infrastructure for a living, at a scale where mistakes get caught by change control, peer review, and someone else’s on-call rotation. My homelab has none of that. If I misconfigure DNS, my household loses the internet and I hear about it immediately.

That is the entire value of the thing. It is where I get to make architecture decisions and then live with them, which is a different kind of learning than reading documentation. The server is called Pelican and it runs Unraid.

The hardware, and why it isn’t impressive

Pelican is a used HP EliteDesk 800 G5 SFF — an Intel Core i5-9500, six cores at 3.0 GHz, in a small-form-factor case that started life under somebody’s desk. Not a custom build, not a rack, nothing chosen from a parts list. I bought it secondhand.

That’s deliberate, and it’s the part I’d defend hardest. The whole machine idles around 50 watts and runs everything below without breaking a sweat — CPU load sits at essentially zero most of the day. A homelab that costs four figures teaches you the same lessons as one that costs three, and the constraint of working with what’s in front of you is closer to the actual job than speccing an ideal build ever is.

Where I did spend, I spent on the things that fail or that hurt when they’re slow:

32 GB of DDR4, half of it genuinely free. ZFS wants memory, containers want memory, and running near the ceiling is how you find out what OOM-killing does to a database at 2am.

Storage, covered properly below — that’s where the real money and the real thinking went.

An NVIDIA Quadro P1000 for hardware transcoding — the low-profile version, because an SFF chassis takes nothing taller. Media transcoding on CPU is the one workload that would actually tax this box; on the GPU it’s free. A cheap used workstation card is the highest-value-per-dollar part in the machine.

That constraint is worth naming, because it’s the real cost of building on an ex-office desktop: one low-profile PCIe slot, four DIMM slots capped at 64 GB, and a power supply sized for an office PC rather than a drive array. Those limits shaped the build more than any preference did.

A UPS, which is the least exciting purchase and the one I’d replace first if it died. It carries about 220 watts of load at 55% capacity, which buys roughly 11 minutes. That is not enough to keep working through an outage and it was never meant to be — it’s enough for a clean, ordered shutdown. An array that loses power mid-write during a parity operation is a genuinely bad afternoon.

2.5 Gb networking, with the two NICs bonded in active-backup. The bond is honestly overkill for a single-homed box on one switch, but it cost nothing to configure and it’s the same pattern I’d use somewhere it mattered.

Total: an ordinary desktop, plus drives, a workstation GPU, and a battery.

Storage: three tiers, on purpose

The array is two 10 TB drives — one data, one parity. Around 6 TB in use.

Parity is not a backup, and I want to be precise about that because it is the most common thing people get wrong about this kind of build. Parity survives a disk failure. It does not survive me deleting the wrong directory, or ransomware, or the house burning down. It buys uptime, not recovery.

In front of the array sits a ZFS mirror of two 1 TB SSDs, acting as the write cache. New writes land on SSD and get moved to the array on a schedule, so the spinning disks aren’t in the path of every small write.

Then the decision I think is the actually interesting one: Docker’s application data lives on a separate 1 TB NVMe drive, mounted outside both the array and the cache pool.

That is not the default, and it took a rebuild to get there. Container appdata is a constant stream of small reads and writes — databases checkpointing, logs rotating, services polling. Putting that on the array means the disks never spin down and every container competes with actual file transfers. Putting it on the cache pool means container I/O fights the write cache for the same devices.

Giving it a dedicated NVMe means the three workloads — bulk storage, write caching, and application I/O — do not contend with each other at all. Storage tiering is not an enterprise-only concept; it just usually gets decided for you by whoever bought the SAN.

DNS: the thing that fails hardest

I run network-wide DNS filtering, and I run two of them — AdGuard Home and Pi-hole — each with its own IP address on the LAN.

That is not indecision. DNS is the single highest-blast-radius service on a home network. When DNS goes down, nothing works, and every device fails at once in a way that looks like the internet is broken rather than like one container is unhealthy. It is also the service most likely to go down during maintenance, because you are updating it, or the host is rebooting.

Two independent resolvers on two addresses means I can take one down, update it, break it, rebuild it — and nothing in the house notices.

Both run on macvlan networking rather than bridge with port mapping, so each gets a real address on the LAN and behaves like a network appliance. Clients point at an IP, not at a host-and-port on the server. That matters because it means DNS keeps working the way clients expect even if I move the container, and because DHCP can hand out those addresses directly.

What actually runs

  • Home Assistant — home automation, on host networking so it can do local device discovery
  • Immich with its own PostgreSQL instance — a self-hosted photo library, and my exit from Google Photos
  • Duplicacy — backups, pointed at the storage share
  • Uptime Kuma — monitoring for everything above; the longest-running service on the box
  • DuckDNS — dynamic DNS, so a residential IP that changes doesn’t break remote access
  • MeTube — local media archiving

Alongside the containers, the box also serves Time Machine targets for the Macs in the house, holds NVR camera recordings, and terminates a WireGuard tunnel for remote access.

That last one is the security decision I’d defend hardest. Not one of the services above is reachable from the internet. No reverse proxy, no published web UIs, no forwarded ports for individual apps. The only thing listening from outside is WireGuard’s single UDP port, and getting past it means holding a key.

The alternative — publishing services behind a reverse proxy — means every one of them is an internet-facing attack surface with its own auth, its own CVEs, and its own update cadence I’d have to keep on top of. Collapsing all of that into one tunnel means remote access is an authentication problem I solve once, instead of a hardening problem I solve per service, forever.

No virtual machines. Everything is containerized, because for this workload VMs would be overhead without a benefit.

Backups, monitoring, integrity

Three habits carried over from work, and they are the part I would actually defend in a technical conversation:

Backups are separate from redundancy. Duplicacy backs up the storage share independently of parity. Parity is for disk failure; backups are for everything else.

Monitoring is not optional just because it’s a home network. Uptime Kuma has been running for ten months and is the only reason I find out a container died from a dashboard rather than from someone in the house telling me.

Integrity gets verified on a schedule, not assumed. Parity checks run scheduled, not ad hoc. The last full check took 15 hours and 40 minutes at an average of 177 MB/s and returned zero errors. A backup or a parity array you have never verified is a belief, not a control.

What it has actually taught me

The lessons that transferred back to my day job were not about Unraid.

Separation of concerns is a performance decision, not just a tidiness one. I understood tiered storage abstractly. I understood it properly after watching container I/O keep an array awake all night.

Redundancy belongs on the thing with the widest blast radius, not on everything equally. One data disk with parity is fine. One DNS server is not — and that asymmetry is a judgment call about consequences, not a rule you can look up.

Monitoring you built yourself is monitoring you actually read. Every alert on that dashboard is one I decided mattered, which is why the dashboard still gets looked at ten months later.

And most usefully: running the whole stack alone means there is nobody to escalate to. Every outage is mine to diagnose from symptom to root cause with no runbook. That is a fundamentally different skill from following an established process, and it is the one I lean on most when something at work breaks in a way nobody has documented.