Infrastructure
The Homelab
A three-node Proxmox cluster and a rebuilt enterprise storage server, running the things I actually use every day. This whole environment is about two weeks old and the server has only been in the rack for one of them, so read it as an active build rather than a finished system. What follows is organized around decisions: the problem, what I weighed, what I chose, and what it cost.
The Cluster
Three nodes, each with a job
Compute runs as a Proxmox VE cluster on repurposed business-class desktops. One node carries the game servers and doubles as a local backup target for files pulled off the storage server. A second runs the Cloudflare tunnel daemon and a Windows VM, which is how I keep a Windows environment while working primarily from a MacBook. The third is racked but not yet online, waiting on a CPU.
Ceph, once the third node is live
The third node exists to make the cluster a real three, which is what Ceph needs for monitor quorum. Two nodes cannot vote sensibly about anything. That is the plan the hardware was bought for, and it is waiting on a shipment rather than on a decision.
Storage
A decommissioned enterprise server, rebuilt
Primary storage is a decommissioned 2U enterprise server rebuilt as the storage and media platform, running Unraid across two dozen enterprise SAS drives with dual parity. Any two drives can fail at the same time with zero data loss. Unraid also lets me mix drive sizes and add capacity a disk at a time, which is how a homelab actually grows.
Pass-through, not hardware RAID
The host bus adapter runs in pass-through mode rather than presenting a hardware RAID volume. That is deliberate: the array stays reconstructible on any Linux machine, with no proprietary controller I would have to source and match if the card died. Tying my data to a specific piece of silicon is a dependency I did not want.
Read the writeup →Mirroring the cache closed the last unprotected component
A mirrored NVMe cache sits in front of the array. Parity writes are read-modify-write across three disks, so sustained ingest would stall without something absorbing it. It is mirrored specifically because it holds live databases and in-flight data that array parity does not cover. Dual parity was already protecting my photos; nothing was protecting the database describing them. That was the last genuinely unprotected thing in the stack.
Read the writeup →Corruption should page you, not surprise you
A daemon watches cache pressure and responds in stages rather than all at once. Separately, files are checksummed against recorded hashes on a rolling schedule, so silent corruption surfaces as an alert now instead of as a file that will not open three years from now.
Distributed Transcoding
Send the work where the work is fast
Video encoding is CPU-hungry, and the server's chip generation predates AVX2, which modern encoders lean on heavily. Per core it loses badly to anything recent. Rather than force the server to grind through encodes, a scheduler farms jobs out to whichever machine is idle: a Mac mini that runs continuously, and a desktop workstation that contributes opportunistically. The server does what it is genuinely good at, holding disks and moving bytes, and hands off the part it is bad at.
Read the writeup →What Runs On It
Immich
A self-hosted photo and video library, with a GPU-accelerated machine learning service doing semantic search, facial recognition, and OCR entirely on-premises. No photo of mine leaves the house to be indexed by somebody else.
Jellyfin
Streaming with hardware transcoding, sharing the same GPU as the ML service. Services release the card when idle so one 4GB GPU can serve both.
Alongside these sit a handful of other containers, plus a media automation stack. I am deliberately not publishing the full inventory.
Network
Segmented, with policy between segments
The network is UniFi-managed and segmented, separating infrastructure, personal devices, IoT, and guest traffic, with explicit policy governing what may cross between them. Segmentation is the single change that made the lab safe to keep running while I break things inside it.
Two ways in, doing different jobs
External exposure is exclusively through Cloudflare Tunnel with Cloudflare Access in front of it. No port forwarding, no dynamic DNS, no inbound hole in the router to find. Separately, WireGuard runs on the gateway as a private mesh back into the network for my own devices. These are not redundant: the tunnel publishes specific public hostnames to the world, WireGuard gets me in privately. Different jobs, different exposure.
Read the writeup →AdGuard
DNS and ad blocking for the whole network.
Aside: local AI inference runs on my desktop machines through LM Studio, not on server infrastructure. It is a desktop hobby that happens to live in the same house, not part of the stack.
Power
UPS-backed, and monitored
The rack is UPS-backed with monitoring, so a brief outage does not turn into an unclean shutdown across a cluster and a parity array at the same time.
What Running This Has Taught Me
Destructive operations fail closed
Anything that deletes requires every precondition to be affirmatively true: a verified successor file, a cryptographic receipt, a link count proving nothing else references it. Uncertainty means do nothing.
Verify the artifact, not the exit code
A job reporting success only proves the job thinks it succeeded. The backup routine dumps the database, checks the compressed output for expected schema markers, and only then promotes it from a partial file to a real filename.
Measure before optimizing
Throughput problems have consistently not been what they first looked like. What presented as a network bottleneck turned out to be parity-write saturation. What looked like too many concurrent jobs was actually a queue of finished work waiting on disk.
Design for the machine going away mid-operation
State is persisted per item, operations are idempotent, and completion is never inferred from a file's absence.
Redundancy is not backup
Dual parity survives hardware failure. It does not survive a wrong path typed by a human.
Backups, Tiered On Purpose
Not everything deserves the same protection
That last lesson is why backups here are split by whether something can be replaced. The irreplaceable tier, photos and the files I have flagged as high value, is backed up offsite to iCloud in compressed form. The reproducible tier, the media library and container and VM state, is covered by array parity and a local backup only, on the reasoning that it can be rebuilt or re-acquired if it is lost.
That is 3-2-1 applied selectively rather than uniformly. Backing up everything offsite would cost more than the reproducible half is worth, and pretending otherwise would mean either overspending or quietly not doing it.
The local copy is in the same building
Locally that means Unraid backup plugins plus a cluster node acting as a backup target. Both sit in the same building as the array, so one bad event at the house still threatens the primary and the local copy together. The irreplaceable tier is safe offsite regardless, which is the part I actually care about, but this is a real limitation rather than a solved problem.
What Is Next
Thinner than it should be
Right now monitoring is whatever UniFi provides built in, plus something specifically watching qBittorrent. That is it. Beszel is planned and not yet deployed. For a two-week-old environment that is survivable, but it is the next thing to fix, because I would rather find problems than be told about them.
What Is Not Here
Security & Privacy
Network topology, internal addressing, segment layout, hostnames, the full service inventory, and version strings are deliberately left out. The architecture and the reasoning are the interesting parts, and they are safe to publish. The map of what runs where is not.