Seven machines, seventy services, and the reasoning in between.
The map above is a homelab — a small cluster of computers in one house, carrying the services a household actually uses: media, photos, documents, chat, DNS, identity. It is built and operated the way a production system would be, at a scale one person can hold in their head. This guide is the written half of that: how to read the wires, what each machine is for, and the decisions that put things where they are.
01 How to read the map hover any box to trace just its wires
Every line is a real pathway between two services, coloured by the kind of traffic it carries. Hover a box to light up only its connections and dim the rest; the switches in the top bar turn each traffic type on and off. Four are shown by default — the ones running constantly.
Showing by default: metrics · logs · alerts · probe. Flip the other four on from the traffic switches in the top bar to see encryption, DNS, login and the Git heartbeat overlaid.
A few boxes have no wires. Nine services are drawn with no pathway attached — not because they are isolated, but because their wiring was not confirmed against a config file when this snapshot was taken. A line on this map means a relationship someone verified in the source; the alternative is a diagram that looks complete and is partly fiction.
02 The seven machines each box in the map is a host
Most of the boxes on the map are a single physical computer; a couple are the network gear — the firewall and the switching fabric — that everything else routes through, drawn here because the lab watches them just as closely as the servers. One machine is the deliberate exception: the Raspberry Pi 5 appears as two boxes. Its monitoring stack and its scheduled control jobs share a single board, but they reboot, fail, and get tinkered with on entirely different schedules — so it is clearer to read them as two separate hosts.
The storage box, and everything that needs to sit next to storage: the media library and its playback server, the photo library, the document archive, the Matrix homeserver, a Git forge, and the smart-home hub. Its processor has no video-acceleration silicon, so transcoding runs in software on the same cores as everything else — which makes CPU contention the design constraint here rather than disk. Two consequences follow. The heavy indexing jobs (photo machine-learning, OCR) are pinned to a subset of cores so they cannot starve playback, and they are deferred wholesale into a weekly overnight window instead of running on arrival. The box is kept deliberately uncrowded for the same reason: every spare cycle is one a transcode can use.
The observability core — the upper of the two Pi 5 boxes. VictoriaMetrics stores every number and vmalert evaluates the rules against it, Loki holds every log, Vector pipes logs into it, and Grafana draws the graphs. Roughly a dozen small exporters translate each device into numbers the store can read. If you want to know the state of anything in the lab, you look here.
This ran on Prometheus until mid-2026. The migration was driven by memory and retention on an 8 GB single-board computer, not by novelty: VictoriaMetrics took over as the authoritative store, ran alongside for verification, and the Prometheus container was removed once its dashboards and rules had been proven against the replacement. Its config layout was kept as-is, because the new store reads it directly and a rewrite would have been churn for its own sake.
The same Pi 5, in its other role. This is the desk that actually gets tinkered at: the identity provider that fronts the private services, the dashboards, and the scheduled jobs that keep the Git repository honest — the hourly sync, the weekly snapshot, the daily dashboard export. Because it is the machine that gets poked and rebooted, the lab is arranged so that nothing critical to noticing problems depends on it staying up.
The dashboard here has been replaced once. A configuration-file dashboard did the job for a year and was retired in favour of hearth, written for this lab specifically, because the remaining work had stopped being configuration and started being interface. The old one was kept running for two weeks after it stopped being used — not out of sentiment, but because its container was still the source of another service's runtime secrets, and decoupling that was a separate change that deserved its own step.
The machine that is deliberately never touched. It carries the jobs that must keep working while everything else is being worked on: network DNS, the HTTPS front door, the uptime monitor, push notifications, and the routing of alerts. It also holds a small self-heal that can restart a logging service on the router when that service dies, and an automatic blocker for hostile source addresses seen at the edge. It runs all of this precisely because it survives a reboot of the collection point — see the second decision below.
The one heavyweight x86 box, always on. It hosts the modded Minecraft server and acts as the spare-capacity pool for anything too demanding for a Raspberry Pi or too CPU-hungry to leave on the NAS — batch jobs get relocated here when they turn out to be competing with media playback. Its monitoring agents report back to the collection point like every other host.
The front gate between the house and the internet. It runs no apps — it routes and filters traffic — but it streams its firewall logs into the collection point and its configuration is snapshotted into the repo every week, so even the gateway is version-controlled.
Not servers — the wiring and Wi-Fi every other machine talks through. They appear on the map because the lab pings them constantly: if a switch or access point stops answering, you want to know before anything plugged into it does.
03 Three decisions that shape everything
Collect everything in one place.
Monitoring is pull-based: one box — the collection point — reaches out and asks every machine for its numbers and logs, rather than each machine needing to know how to reach the dashboards. Adding a host is then a one-line change on one machine instead of a deployment to the new one. The cost is a single busy dependency, which the next decision exists to contain. This is the heaviest traffic in the lab, and why it is on by default above.
Keep the alarm on its own circuit.
The thing that tells you something is broken must not share a fate with the thing
you are most likely to break. So alert routing, DNS, TLS and uptime checks live on
pi4 — the machine nobody touches — while metrics and logs live on the Pi that
gets actively worked on. Reboot the collection point and the alarm keeps watching. The
trade is that two Pis must be kept healthy instead of one; the failure it prevents is the
one where the monitoring goes down and nothing says so.
An alert that can't fire is worse than none.
A rule that says “alarm if the last reading is older than ten minutes” silently stops working the moment readings stop existing — the expression returns nothing, and nothing is not an alarm. Every deadman check here is written to fire on absence explicitly, because the quiet version had been failing open. The same rule applies to this page: instrumentation you have not tested is a claim, not a safeguard.
04 The repo is the source of truth GlassBaby/homelab @ main
The lab is defined as code in one Git repository: containers, config files and scheduled jobs exist first as version-controlled text, which makes the repo — not any single machine — the authoritative record of how the system is meant to behave. To answer “what is this lab?” you read the repository, not the running hosts. The honest version of that claim carries an asterisk: a handful of services predate the convention and were built by hand on the box they run on. They are tracked as work to bring in, not quietly counted as compliant.
The mechanism that keeps that claim true is a closed loop, not a one-way deploy. Configuration is committed and rolled out to the hosts in the usual direction; an hourly job then runs the reverse, reading the running state of each machine and committing it back. When something drifts — a value changed by hand, a container updated in place — the next sync records the difference as a commit. The gap between what the repo declares and what is actually running is therefore measurable rather than assumed — for the paths on the sync allowlist. Anything outside it surfaces as a dirty working tree for review instead of being committed automatically, which is the deliberate trade: an unattended job is trusted to record drift, never to decide what counts as intentional.
Secrets are age-encrypted with SOPS before they are ever written to Git, so the repository can hold the complete system — including the encrypted material — without exposing a credential. That is what makes “the repo is the system” a workable claim rather than an aspirational one: there is no second, secret repository where the real configuration lives.
One honest caveat, because the alternative is the kind of claim this page used to make. The map above is not generated from the repository — it is a hand-curated snapshot, dated at the bottom of this page, and it is accurate as of that date and no later. An earlier version of this guide asserted that the diagram “cannot disagree with the code that produced it.” It could, and it did: it went on showing a metrics engine and a dashboard for weeks after both had been decommissioned. Generating it from the repo is the obvious fix and is a known piece of outstanding work. Until that exists, the date is the disclaimer.
describe in code
Every service is a compose file and a config, kept as plain text in the repo.
pull reality back
An hourly timer captures what's actually running and commits it, so the repo stays truthful.
secrets stay sealed
Passwords and keys are SOPS-encrypted before they're stored, plus weekly config snapshots.
snapshot, dated
This topology is transcribed from main by hand and stamped with the date it was read.