Everything I self-host runs on a two-node Kubernetes cluster at home. One git repository describes all of it. The rule I hold it to: I should be able to wipe both machines and get the same cluster back from that repo.
| helena | Control plane. 16 threads, 32 GB RAM, an RTX 3060 Ti with 8 GB. Runs the agents, the observability stack and this site's API. | ubuntu 24.04 |
| goku | Worker. Dell OptiPlex 3060, i3-8100T, 32 GB RAM. Runs the family media library. | ubuntu 22.04 |
goku doesn't power back on by itself after an outage. helena does. When every media app sits in Pending for a day, that's the first thing I check.
No port is open on my router. A Cloudflare Tunnel runs inside the cluster as a normal deployment and dials out to Cloudflare, so traffic arrives over a connection my side started. TLS ends at Cloudflare's edge.
From the tunnel, Traefik (it ships with k3s) routes by hostname to the right service. Admin interfaces sit behind Cloudflare Access, and anything that should never be public, like the search engine my agent uses, has no ingress at all. For the rest I use Tailscale.
Argo CD, app-of-apps. One root application points at a folder of applications, and each of those points at the manifests for one component. A change is a pull request: I merge it, Argo syncs it. Nothing gets applied by hand.
Secrets live in the same repo as Sealed Secrets, encrypted so only the cluster can read them. Every image is pinned to a version and a digest, and Renovate opens the pull requests to move them forward. Updates stay visible and every state is reproducible.
For my own projects the loop goes one step further: CI builds the image, publishes it to GitHub's registry and commits the new digest to the manifest. Argo picks it up from there. This site's API ships that way.
Tap a service to see what it does and what it talks to. The toggle above switches between the plain and the technical explanation.
October 2026: 37 Argo CD applications, about 75 pods. A few private ones aren't shown.
AI assistant
Watching things
My projects
Keeping it running
Movies and shows
command: is not what docker-compose taught you. In Kubernetes it replaces the image's entrypoint. To pass arguments and keep the entrypoint, use args:. My agent's gateway didn't start until I learned this.
A Service can quietly overwrite an app's config. Kubernetes injects variables like SEARXNG_PORT=tcp://… for every Service in the namespace. If the app reads a variable with that name, it breaks. enableServiceLinks: false fixes it; it hit SearXNG and Ollama.
pgvector's HNSW index stops at 2,000 dimensions. I picked an embedding model that fits under it instead of fighting the index.
Two agents writing to one git repo will conflict. The shared memory vault syncs every five minutes from a CronJob. When a sync conflicts, the commits are parked on their own branch and the next session warns me, instead of one side silently winning.
I run platforms for a living. This is where I try things with my own time and my own hardware before I suggest them at work: new observability stores, load tests for my own tools, new ways to ship. It also means my family's shows don't depend on someone else's pricing page.