The infrastructure
a home cluster, run like production
Most of the projects on this site are deployed by the same chain, on a small Kubernetes cluster split between a rented VPS and a machine at home. This page is the cut-away drawing, what runs where, how a commit becomes a pod, and what stays inside.
The cluster
Two nodes, one K3s cluster. The rented VPS carries the control plane and is cordoned: it schedules nothing, so its eight shared vCPUs go to the things that must sit next to a public address: the game servers, which run natively outside Kubernetes on purpose, and a read-only collector that watches them. The machine at home, a 24-thread desktop, is the only worker and runs every workload: about thirty pods at last count, with local volumes and a MinIO instance for object storage.
The two are joined by a WireGuard tunnel with a round-trip of about fifteen milliseconds. The pod network does not cross it, which is a constraint I chose to live with: anything the VPS needs from a pod goes through a node port handled locally. Ten applications are active: a Minecraft supervision panel, the distribution stack of a game (site, update server, backend, database, cache), a private registry, Jenkins, an embeddings service that scales to zero, MinIO, a ticketing sandbox, a couple of static sites, and thirteen more are archived with their charts and database dumps kept.
The delivery chain
One git repository is the source of truth for the whole cluster. Argo CD runs in app-of-apps mode over it, with prune and self-heal on: whatever the repository says is what runs, and nothing can be paused from the cluster side. Each application is a small Helm chart copied from a commented template rather than a third-party chart, so every value is one I wrote.
Building happens in the cluster too. Jenkins, itself a chart, runs a pipeline in a pod for each application repository: build the image (in a pod, with kaniko or a Docker sidecar), push it to the private registry, then clone the infra repository, rewrite the image tag in that application's values file and commit: deploy(app): tag. That commit is the deployment; Argo notices it, templates the chart, applies it. Rolling back is a git revert. Cert-manager issues TLS from Let's Encrypt, KEDA puts rarely-used services to sleep and wakes them on the first HTTP request, and a scheduler sends whole applications to sleep at night.
Network, secrets and storage
Traffic enters through Cloudflare DNS (proxied for HTTP, direct for the game ports), reaches the VPS on ports 80 and 443, is forwarded through the tunnel to Traefik running on the host network of the home node, then Ingress, Service, pod. The API server itself is not part of that path.
Secrets live in git, sealed: encrypted with a controller's public key, decrypted only in the cluster, with the master key kept offline and never on either node, twenty-five of them at the moment. Backups are two jobs: a nightly pg_dump of the panel's database, verified with a restore listing before it is pushed to MinIO with a fourteen-copy rotation, and the game server's own full backups shipped over the tunnel by a systemd timer, written locally first and pushed afterwards so the network is never in the freeze path.
What it taught me
Three things that no tutorial says. Self-heal is a commitment: retiring an application became a repository convention (a dotted directory, a removed Application, a dump, a dated line) because nothing can be stopped by hand. Host-network ingress, ExternalName services and Argo's resource exclusions each have one silent failure mode, and the only durable defence has been a numbered list of traps kept next to the code. And observability is not free: I removed Prometheus, Grafana and Loki to give 1.6 GB back to a game server, measured that it changed nothing about the game, and now have a panel that keeps its own time series instead. Putting monitoring back is the next item on the list.