My home server, cs, runs file sync, media, a household budget, dashboards, a Magic deck lab and the staging copy of my NFL platform in about fifty containers. Small cloud machines around it handle sign-in, monitoring, Plex’s internet front, the NFL platform in production and my WordPress sites. All of it lives in one Git repository, watched from outside the house. Since October it also has an RTX 3060 and runs the gateway of a local AI cluster that the household’s tools use.
The problem
An earlier version of this setup lost its repository’s remote while the server was edited in place, and drifted 77 files unnoticed. Nothing proved that Git matched what ran. And one machine at home was a single point of failure for sign-in and alerts.
What I built
- One repository for every host. Each service on
cshas its own Compose file, and oneinclude:list decides what runs. The cloud machinesauth,watch,api,id,plexfrontandwphave their own. - The checkout is the configuration. A deploy script fast-forwards
main, validates and starts the containers, and refuses a dirty tree. - An hourly drift check compares each host’s checkout with GitHub and turns a monitor red when they differ.
- Three validation gates: a pre-commit hook that refuses credential-shaped lines, CI that validates every host’s Compose project on every push, and the deploy script’s own check.
- The same hardening everywhere: cloud firewalls, one SSH configuration, daily checks for failed services, pending reboots and published vulnerabilities, patching with one script, and a monthly disk self-test.
- Sign-in on its own machine. authentik runs on
id, configured in Git as blueprints, and every account sets up a second factor at first login. I moved sign-in to it in phases.
How it works

A request from outside the house goes to Cloudflare, down a tunnel the server opens outbound, then to Traefik, the reverse proxy on cs. Every zone uses TLS Full (strict), and Cloudflare Access is checked twice. Traefik adds a rate limit, security headers and, on most routes, a CrowdSec block list whose allowlists live in Git; Cloudflare does the country block. Where a rule requires it, Traefik asks authentik whether you’re signed in, and hands the request to the container. Plex is the exception: its traffic goes through plexfront, off Cloudflare’s proxy. At home, both local DNS resolvers point every service name at the proxy, so that traffic never leaves the house.
Credentials are files outside Git, mounted per service. The WordPress sites and Nextcloud send mail through Amazon SES.
Prometheus, Loki and Alloy feed Grafana metrics and 90 days of logs. Uptime Kuma runs off-site on watch, where it probes the public addresses and serves the status page; every scheduled job has to report in to it. Alerts reach my phone through ntfy, also off-site.
The household budget runs in Actual Budget, with a scheduled bank sync through SimpleFIN. A small parser reads statement emails arriving through Amazon SES and fills in what is due; payee rules run weekly, and a Grafana dashboard shows the bills.
Nightly jobs on cs back up its configuration, the Nextcloud database, the budget, the deck lab’s database and the api, auth, id, watch and wp machines to a separate disk. Each verifies its copy before naming it good and reports to a push monitor, so a silent job turns red within 26 hours.
Decisions and trade-offs
- The server’s checkout is production. A tool that pushes a copy can quietly diverge. The cost is discipline: no branches on the server, and a host-only fix is pushed at once.
- A tunnel instead of open ports. I gave up direct inbound connections, so nothing at home listens to the internet.
- Sign-in, status page and alerts off-site. Keeping them on
cswould mean fewer machines, but an alert aboutcsshouldn’t needcsto deliver it. - Disk pools, not RAID, for media. mergerfs keeps each file whole on one disk, so a dead disk loses only its own files, and media can be replaced.
What’s next
Authelia, which authentik replaced, still runs. It retires on a reading, not a date: its login log must show no successful sign-in over a span of normal use. After that, a second media host: pack the media onto fewer disks and move the freed ones to a new machine with a RAID array, which will also hold a file-level Nextcloud backup.
Built with
Ubuntu, Docker Compose, Traefik, Cloudflare Tunnel and Access, authentik, Authelia, WireGuard, AdGuard Home, CrowdSec, Prometheus, Grafana, Loki, Alloy, Uptime Kuma, ntfy, Nextcloud, Actual Budget, SimpleFIN, Amazon SES, mergerfs, GitHub Actions, DigitalOcean.