The home server — five VMs, one hypervisor

What it is
Everything I run — this site, MyDash, Modus, Home Assistant, 35+ self-hosted services — lives on one iMac acting as a UTM hypervisor for five VMs, all on one LAN. No cloud compute for anything except DNS/CDN edge (Cloudflare) and this backend’s public routing.
Features
A dedicated VM per concern that needs isolation: the main Ubuntu Server VM runs almost everything as Docker containers; a separate CloudPanel VM (no Docker) runs this site’s Astro frontend and Payload backend, deliberately lighter-weight than the Docker/Coolify pipeline it replaced; Home Assistant OS gets its own VM; an isolated Coolify VM is kept around purely as a rollback target for the site’s old deployment. Every VM shares one LAN, one DNS resolver (Technitium), one reverse-proxy layer (BunkerWeb + Nginx Proxy Manager), and one SSO gate (Authentik) in front of almost everything.
Why I built it
Self-hosting everything means owning every failure mode too — so the architecture optimizes for "an agent or I can diagnose this at 11pm without re-deriving how it works." Durable topology lives in one wiki (Cortex); live state is never hand-copied into a doc, it’s always computed fresh from the running system.
How it works
LAN traffic resolves through Technitium, hits BunkerWeb’s WAF (CrowdSec-backed), then Nginx Proxy Manager, then Authentik’s forward-auth gate, then the target container. Public traffic for joecastagna.com never opens a WAN port on any VM directly — it comes in through an existing Cloudflare Tunnel connector and lands on the CloudPanel VM over the LAN. A NetBird VPN peer gets the same LAN routing from anywhere, via split-horizon DNS.
How it was built
Each VM was added only when a real isolation need showed up — the CloudPanel VM specifically because a Docker/Coolify rebuild pipeline (2-4 minutes minimum) was the wrong cost for small personal-site edits. A pre-hardened, empty VM clone sits dormant in UTM specifically so the next VM doesn’t need a fresh install from ISO. Every host, port, and routing rule is documented once in Cortex and never duplicated — live status is a Cerebrum API call, not a paragraph someone has to remember to update.
