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
- Five Ubuntu Server VMs in one Docker Swarm — one manager, four workers — spread across two Proxmox hypervisors.
- Nodes range from 4 vCPU / 3 GB RAM up to 12 vCPU / 16 GB, roughly 36 vCPUs and 41 GB of RAM across the cluster.
- 50+ services, pinned to nodes with Docker node labels and placement constraints so stateful workloads land on the hardware sized for them.
- One node carries an NVIDIA GPU passed through for Plex hardware transcoding.
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
- Traefik v2 is the single ingress. Every service gets a subdomain, and TLS comes from Let’s Encrypt via Cloudflare DNS challenge — no per-service certificate management.
- The Docker bridge runs on a custom IP range because the default conflicted with my LAN subnet. That one bit me before I understood it.
- Overlay networks are split by concern — proxy traffic, log shipping, and application traffic each get their own network.
- Upstream of the swarm, the LAN is UniFi-managed with VLAN segmentation; the controller lives on a separate edge box so the network survives the compute host going dark.
Observability
- Prometheus + Grafana are the primary pane of glass; node exporters run on the swarm nodes, both hypervisors, and the NFS server, so I can see the whole stack down to the metal.
- Uptime Kuma watches service availability; Smokeping tracks network latency over time.
- Every stack file ships container logs via the GELF driver to a central collector. I ran a full ELK pipeline on top of that for a while and retired it — at homelab scale its resource appetite outweighed what it returned.
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.