catch { snail }

The scaling ladder: a production setup that scales by not scaling yet

February 14, 2021

Since the terraform destroy post, the most common question in my inbox is some version of: “cute, but will your €5 setup scale?”

I think that’s the wrong question, and this guide is about the right one. Because here’s the dirty secret of every “scalable production architecture” diagram: you don’t get taken down by load. You get taken down by the rewrite you scheduled for the week the load arrived.

So the metric I design for isn’t requests per second. It’s minutes to the next rung — how long it takes to climb one step up when I actually need to. Scalability isn’t a building you construct upfront. It’s a series of doors you deliberately leave unlocked.

Here’s the ladder, rung by rung, with each unlocked door pointed out.

Rung 0: the €5 baseline (you are here)

Recap for new readers: Terraform builds a Hetzner VPS and DNS, cloud-init installs k3s, and the cluster pulls its manifests from git. The entire company — population: me — fits in one repo, and the whole environment rebuilds from zero in under five minutes, which I know because I destroy it on purpose quarterly.

That ritual matters for this post: a rehearsed rebuild is also a rehearsed migration. Every rung below is cheap precisely because reconstruction is a habit, not an emergency.

Rung 1: the app must be a leaf, not a root

Nothing else on this ladder works unless the application itself is stateless: config from environment variables, sessions in a store rather than in memory, nothing precious on local disk, logs to stdout. Old news — the 12-factor checklist is a decade old — but it’s the rung people skip and then blame Kubernetes.

The test is brutal and simple: kubectl scale deployment app --replicas=3. If anything breaks — sessions vanish, uploads disappear, a cron job runs three times — you found state hiding in your app. Evict it now, while it’s small.

Door unlocked: horizontal scaling becomes a number in a YAML file instead of a project.

Rung 2: shrink what counts as state

The Terraform post’s lesson keeps paying rent: after a destroy/rebuild, everything that came back automatically was never really state — it was opinion in git. Scaling is the same audit. Manifests, certs, DNS, the cluster itself: opinions. What’s left — the database and user uploads — is the actual state, and the strategy is to keep that pile as small and as boring as possible.

Concretely:

  • Uploads go to object storage from day one (Hetzner Storage Box, Backblaze B2, whatever’s cheap). S3-compatible APIs are the closest thing infrastructure has to a universal adapter, and this is the cheapest optionality purchase on the entire ladder.
  • The database gets a thin seam. I don’t mean an abstraction layer for switching databases — you won’t. I mean: connection string from env, schema in migration files, backups tested by restoring them (restic + a monthly restore drill). SQLite→Postgres, or self-hosted Postgres→managed Postgres, are then evenings, not quarters.

Door unlocked: the only hard migration in your future is one small, well-mapped pile instead of “the server.”

Rung 3: the cluster grows by invitation

The reason I picked k3s over docker-compose finally cashes out here. Adding a worker node is:

# on the new €5 box (which Terraform created with count = 2)
curl -sfL https://get.k3s.io | K3S_URL=https://snail-01:6443 K3S_TOKEN=$TOKEN sh -

That’s it — the node joins, the scheduler starts using it. And when the control plane itself needs to survive a node death, recent k3s can run an embedded etcd cluster across three servers instead of single-node SQLite. Same binary, same manifests, zero application changes.

This is the rung that justifies the YAML tax from last summer: compose scales a machine; an orchestrator scales a fleet, and the manifests can’t tell the difference.

Door unlocked: capacity becomes count = N in Terraform plus one join command.

Rung 4: the front door gets a receptionist

Right now DNS points at the node. One Terraform resource swaps that for a Hetzner load balancer pointing at all nodes, health checks included. Because rung 1 made the app stateless and rung 3 made nodes interchangeable, this is genuinely just a routing change — the LB is boring because the lower rungs did their jobs.

Door unlocked: nodes become anonymous. The uptime alert stops being about any particular machine.

Rung 5: observability, or, receipts

You can’t climb a ladder you can’t see. Prometheus + Grafana for metrics, Loki for logs — the kube-prometheus-stack Helm chart makes this an afternoon, and on a small cluster it costs more RAM than it has any right to, and it’s still worth it.

Here’s the framing that made me stop treating monitoring as a chore: capacity plans are beliefs, and metrics are how beliefs meet evidence. Same epistemology as terraform plan, same failure mode as a language model inventing APIs — a system confidently describing a reality it’s never checked. “We’ll scale when we need to” is a hallucination until a dashboard says when need is. My rule: I climb a rung when a graph tells me to, never when anxiety does.

Door unlocked: scaling decisions become diffs against data instead of vibes.

What the ladder refuses to include

No multi-region, no service mesh, no autoscaling, no microservices. Not because they’re bad — because each is a building, not a door, and buildings are premature until a graph screams. The most scalable decision I’ve made all year is the stuff I declined to install. Premature scaling is the root of all cloud bills.

And the honest coda: I’m on rung 0 and expect to stay here. The ladder’s real product was never capacity — it’s that every rung is rehearsed, so growth is a checklist and not a crisis. I sleep well next to an unlocked door.

Next

The itch from October remains: git already holds the manifests, so why am I the one running kubectl apply? The GitOps rabbit hole is officially scheduled.

Also, a certain API invite finally showed up in my inbox this week. The silly benchmark gets its rematch. Stay tuned.


My personal notes.
I write about code.

© 2026, Built with Gatsby and a tiny Snail