Skip to main content

The Kubernetes Backlash Is Right — Just Aimed at the Wrong Layer

9 min readDora NodaDora Noda
Share

The 2026 consensus on Kubernetes flipped hard. CNCF-adjacent surveys now put the number at 58% of organizations reporting that Kubernetes maintenance eats more than 30% of their platform team's time, a 2026 startup survey found 78% of teams hit "significant operational challenges" within six months of adopting it, and the going rule of thumb is that you need a cloud bill north of $2M a year before a three-person, $600K/year platform team even pencils out. Docker Compose is having a quiet renaissance. Hacker News threads about ripping Kubernetes out and shipping on Compose instead routinely land on the front page.

Here's the part that argument skips: bex — the self-hosted, git-push PaaS this blog belongs to — runs Cluster API under every deployment, on real owned Hetzner hardware, and none of those numbers are an argument against it. That's not a contradiction. It's because "Kubernetes" in that backlash means two different things wearing one name, and the complexity the survey data is measuring is the one bex never hands you.

Two things called "Kubernetes"

The first is Kubernetes exposed directly to a developer: writing and maintaining Deployment, Service, Ingress, and HorizontalPodAutoscaler YAML by hand, packaging Helm charts, granting and auditing RBAC, running an ingress controller and a cert-manager stack, and keeping a person on call who can read a kubectl get events output at 3 A.M. and know what it means. This is the Kubernetes the 2026 surveys are describing, and they're right about it — it's a real, ongoing operational tax that most teams shipping a web app or an API don't need to pay.

The second is Kubernetes — specifically Cluster API — as the layer that manages the physical machines underneath a platform: detecting that a server died, provisioning a replacement, and rescheduling what was running on it, all without a human watching. This is a different problem with a different owner. It doesn't show up in a developer's terminal at all; it shows up in whether the platform they push to keeps running when a piece of hardware fails while everyone's asleep. The backlash never argues against solving this problem — it argues against making every developer solve the first one to get the second one solved for free. Those are not the same ask, and conflating them is why "just use Compose" and "we run Cluster API under bex" can both be correct in the same paragraph.

What the backlash is actually rejecting

Put a plain containerized web app on raw Kubernetes and count what you touch before it's live: a Deployment manifest, a Service, an Ingress resource pointed at a controller you installed and configured yourself, a cert-manager Issuer and Certificate for TLS, probably a HorizontalPodAutoscaler if you want it to scale, and a values file if any of it comes from a Helm chart instead of raw YAML. That's five to seven separate objects, each with its own schema, before you've shipped one route.

Every one of those keeps changing after you write it — cert renewals, ingress-controller version bumps, HPA tuning — and every change is a YAML diff someone has to understand and be willing to apply to a running cluster.

Put the same app on bex: a bex.yml with a name, an image, a port, and a health check path, and git push. One file, one command, an HTTPS URL back. The gap between those two workflows — not "Kubernetes vs. no Kubernetes," but "YAML you maintain forever vs. a file you write once" — is the entire complexity the backlash is measuring. It's a real gap. bex agrees with the backlash on it completely, which is why a developer pushing to bex never sees a Deployment object, an Ingress resource, or an RBAC policy. They see a git remote.

The problem that doesn't go away: what happens when a node dies at 3 A.M.

Here's the concrete case the backlash's own framing walks past. Say you're running three owned Hetzner boxes — not a hyperscaler autoscaling group, real machines you bought — because one server isn't enough capacity or isn't enough redundancy for what you're running. At 3 A.M., one of them's PSU fails. What happens next depends entirely on what's managing the fleet, and the two answers aren't close.

On Docker Compose, Kamal, or a single-host PaaS, there is no fleet layer — each host runs its own Compose stack independently, with no controller watching across machines. When that box goes dark, everything on it goes dark with it, and nothing in the stack itself notices. Detection depends entirely on whatever external uptime monitoring you separately wired up; remediation means a human gets paged, SSHes into a different box (assuming they provisioned one), and manually starts the affected containers there, or waits for the original hardware to come back. There's no bound on that number — it's 20 minutes if the on-call engineer is at their laptop, hours if they're asleep and the alert doesn't escalate cleanly.

On a Cluster API fleet — what bex runs underneath — a MachineHealthCheck resource is already watching every node's Ready condition. Mark that condition False for longer than the configured timeoutSeconds (300 seconds, i.e. five minutes, is Cluster API's documented example threshold — configurable but not something you write by hand per incident) and Cluster API stops waiting: it taints the node NoExecute, and Kubernetes' own pod-eviction machinery reschedules everything that was running there onto the fleet's surviving nodes, automatically, inside that same five-minute window — provided those nodes have the spare headroom to absorb it. That headroom is a real, non-free operational commitment (a responsibly run fleet keeps it on purpose), not a hand-wave — it's the price of the automatic part.

Replacing the physical capacity the dead node represented is the slower half, and it's honest to say it isn't instant. CAPH's own remediation for a bare-metal host tries a reboot first — often enough to recover a hung or crashed box in a couple of minutes — and only falls back to fully unprovisioning and re-provisioning the host, via Hetzner's rescue system, if the reboot doesn't bring it back. That fallback path is a genuine bare-metal re-provision cycle, realistically fifteen to twenty-plus minutes for a real dedicated server, not a cloud API call.

Nothing about Cluster API teleports a dead PSU back to life faster than a human could. What it does is run the two things that matter — evicting workloads off a node that stopped responding, and eventually replacing the box — as a controller loop that doesn't need anyone awake, instead of as a page.

Compose / single-host PaaS on a multi-node fleetCluster API fleet (bex)
Detects the dead nodeOnly if you separately wired up external monitoringMachineHealthCheck watching Ready, built in
Time to reschedule affected workloadsUnbounded — depends on a human noticing and acting~5 minutes (taint-based eviction), if surviving nodes have headroom
Time to replace the physical capacityUnbounded — human provisions or repairs the boxReboot attempted first (~minutes); full re-provision if that fails (~15-20+ min on real bare metal)
Who's involvedOn-call engineer, awake, at a keyboardNo one, until capacity needs restocking

That table is the whole argument: the backlash is correct that most teams don't need to hand-write the YAML in the first section. It has nothing to say about whether the fleet underneath should notice a dead machine on its own — and on more than one owned host, something has to.

What a bex developer touches vs. what's actually running

The developer-facing surface of bex is deliberately small: a bex.yml manifest, a git push, and a Render-compatible API an AI agent can call directly to deploy or check status. That's it — no Deployment spec, no Ingress rule, no RBAC binding, no Helm values file ever crosses that surface.

Underneath, a real Cluster API object graph is reconciling continuously: a Cluster resource, a MachineHealthCheck per node pool, and either HCloudMachine objects (for Hetzner Cloud VM capacity) or HetznerBareMetalHost/HetznerBareMetalMachine pairs (for real dedicated servers) tracking every physical or virtual machine in the fleet. None of it is hidden because it's embarrassing — it's hidden because it's not the developer's problem, in the same way a Render or Heroku user was never expected to understand the hypervisor under their dyno. The difference is that on bex, "under the hood" means you can kubectl get machines and see the actual fleet, on hardware your organization owns, if you ever need to — not a black box behind someone else's API.

Where the line actually sits

The $2M-cloud-bill threshold circulating in 2026 commentary is a proxy for the wrong variable. The variable that actually matters is simpler: are you running on one host, or more than one? On one host, Compose is the right answer and bex doesn't argue otherwise — there's no fleet to manage, so there's nothing for Cluster API to do. The moment you own a second host — because one machine isn't enough capacity, or because you want the app to survive a hardware failure without someone waking up — you need some layer watching across machines and deciding what happens when one of them stops responding. Hand it to Cluster API, which multiple providers have already spent years hardening for exactly this job, or hand-roll the same detection-and-remediation logic yourself, or pay a hosted platform's markup to not think about it. Cluster API already exists and already works; hiding it behind a git push is the option that costs a developer nothing to get.

bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, with a real Cluster API fleet handling node failures underneath so you don't have to. Star the repo on GitHub or deploy your first app today.

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex