Project · ongoing

Self-hosted infrastructure

A private cloud run to production standards: a 5-node Docker Swarm on two Proxmox hypervisors, 50+ services, NFS storage, Traefik at the edge, and Prometheus-based monitoring.

This is the platform everything else I build at home runs on — including this website and the Hermes agents. It’s a private cloud operated to production standards: infrastructure as code, tiered failure domains, monitoring that pages before users notice, and a deploy pipeline with a human approval gate.

Topology

Stacks tiered by criticality

Services are grouped into stacks by how much their failure hurts, not by what they do. The data layer (databases, metrics storage) deploys first; access portals, surveillance, and media/apps follow; the reverse proxy fronts it all. The payoff is operational: I can drain and patch a media node without touching a database, and when something breaks, the blast radius is already drawn.

Storage

A dedicated NFS server running OpenMediaVault exports three shares — service data, media, and camera footage — mounted on every node. Because persistent state lives on NFS rather than on any one VM, Swarm can reschedule most services onto another node and they come back with their data.

Edge and networking

Observability

Configuration sync

All five nodes stay in sync through a bidirectional rsync job that runs every 10 minutes from cron: scripts, stack files, and service configs. I can edit a config on any node and it propagates everywhere — with node-specific configs deliberately excluded. A systemd unit brings the whole cluster up in dependency order, data layer first.

The one Compose exception

Media automation (the ARR stack) routes all its traffic through a WireGuard container using Docker’s network_mode: service: — every app shares the VPN container’s network namespace, so if the tunnel drops, nothing leaks. Swarm doesn’t support sharing a network namespace between services, so this one stack runs under plain Docker Compose on a single node while everything else is Swarm. Knowing where your orchestrator’s model ends is worth more than forcing everything into it.

How this site ships

The site you’re reading is an Astro static build, running as two replicas behind Traefik on the cluster. Code lives on self-hosted Gitea; main is a protected branch — PR-only, one approval required — and Woodpecker CI builds and deploys on merge. Custom images are pushed to a local Docker registry on the manager node. Analytics is self-hosted, cookieless Umami; the contact form sits behind Cloudflare Turnstile and feeds an n8n workflow into a self-hosted CRM with a Discord notification.

Nothing here is a toy deployment. The point of the lab is that the patterns — tiered failure domains, GitOps, single ingress, metrics-first observability — are the same ones I use at work, practiced on infrastructure where I own every layer.