Skip to main content

K3s vs K0s Under Cluster API: Which Lightweight Distro Actually Fits a Hetzner Fleet's Machine Lifecycle

9 min readDora NodaDora Noda
Share
On this page

Every k3s-vs-k0s comparison on the internet is written for the same reader: someone with one Raspberry Pi, deciding which single binary to curl | sh onto it. That reader cares about install size and bundled add-ons. A platform standing up node pools on Hetzner through Cluster API is not that reader — and the comparison that actually matters for them isn't in any of those posts.

Here's the fork that matters: k3s's Cluster API provider runs the control plane as a real Machine on a real Hetzner box, reconciled the same way as every worker. K0s's Cluster API provider, by default, runs the control plane as pods inside your management cluster — not on a node CAPH provisioned at all. That's not a footprint difference. It's a different answer to "whose blast radius is this cluster's control plane," and it decides more about a multi-tenant fleet's operations than either binary's megabyte count.

Two Distros, Two Different Places the Control Plane Actually Runs

Cluster API needs three provider pieces to turn a Machine object into a running node: infrastructure (which box), bootstrap (what cloud-init turns it into a kubelet), and control plane (how the API server gets stood up and kept healthy). CAPH — Cluster API Provider Hetzner — is the infrastructure piece either way. The distros diverge entirely at the other two.

k3s plugs in through k3s-io/cluster-api-k3s, which ships KThreesControlPlane and KThreesTemplate CRDs. Point it at CAPH, and each control-plane replica is a Machine — a real Hetzner Cloud server or dedicated box, provisioned, given a cloud-init script that installs and starts the k3s server binary, and reconciled by the same Machine controller watching every worker in the fleet. Scale the control plane from one replica to three, and CAPH provisions two more Hetzner servers. It's the stacked-control-plane pattern Cluster API was designed around from day one, just with k3s server instead of kubeadm doing the bootstrapping.

k0s's primary Cluster API path is k0smotron, and its default mode is architecturally different: K0smotronControlPlane doesn't create Machines for the control plane at all. It runs kine + the k0s controller as pods inside your existing management cluster, and only uses a bootstrap provider to join real Hetzner machines as workers. That's the hosted-control-plane pattern — the same shape as EKS or GKE's managed control plane, just self-hosted inside your own mgmt cluster instead of a cloud vendor's. (k0smotron also ships a remote-machine provider that can install k0s directly onto real boxes over SSH if you want a self-hosted control plane instead — but that's the opt-in path, not the default one most CAPI-on-Hetzner setups reach for.)

Put plainly: ask for a 3-node HA control plane on a CAPH fleet, and k3s gives you three more line items on your Hetzner invoice; k0smotron gives you three more pods on a cluster you're already running. Neither is a bug. They're a real fork in where the tenant's control-plane compute — and its failure domain — physically sits.

What Each Distro's Defaults Actually Cost You on a CAPH Node Pool

Binary size is the number every comparison leads with — k3s at under 100 MB, k0s closer to 165 MB — but it's the least relevant number for a fleet that isn't billing by the megabyte of a control-plane image. What costs real engineering time is what each distro decided to bundle for you, because a CAPH-based platform standing up dozens of node pools needs those decisions to be consistent and controllable across every one, not eyeballed per cluster.

k3s ships opinionated: containerd, Flannel, CoreDNS, metrics-server, a local-path storage provisioner, Traefik as the default ingress, and ServiceLB as the default LoadBalancer implementation — all installed and wired together the moment k3s server starts. On a CAPH fleet running its own ingress and load-balancing story (most self-hosted PaaS platforms do — you don't want Traefik's opinions fighting your own), every one of those needs to be turned off explicitly, and KThreesControlPlane's serverConfig is where you do it:

yaml
spec:
  kthreesConfigSpec:
    serverConfig:
      disableComponents:
        - traefik
        - servicelb

That's a small, declarative fix once you know it exists — but "once you know it exists" is exactly the gap that bites a fleet standing up its hundredth cluster from a template nobody's re-audited since cluster one.

k0s ships blank: no bundled ingress controller, no bundled LoadBalancer implementation, and (as of recent releases) still no local-path provisioner by default. The default CNI is kube-router, swappable to Calico via config. There's nothing to strip out — but there's also nothing running until you deploy a CNI, ingress, and load-balancer yourself, on every cluster. For a platform that already runs its own ingress and load-balancer stack across every tenant cluster — which a self-hosted PaaS almost always does — that blank slate is a wash either way: k3s costs a few disable flags in the KThreesControlPlane spec; k0s costs the same YAML you were always going to apply, just with nothing pre-installed underneath it to conflict with.

The place the defaults genuinely differ is the datastore. k3s defaults to SQLite for a single control-plane replica and switches to embedded etcd automatically once you scale past one — etcd runs inside the k3s server process on each control-plane Machine, no separate pod or binary. k0s defaults straight to etcd, always, matching upstream Kubernetes' own assumption and giving up SQLite's simplicity for a single-node dev cluster in exchange for never having a "surprise, you're on a different datastore now" migration moment when you scale to HA.

Cert Rotation and Upgrades: Which Reconcile Loop Actually Fits CAPI's Model

This is the question the TODO item asks and most comparisons skip entirely, because it only shows up once a distro is being reconciled by Cluster API instead of installed by hand.

k3s's cert story is the more CAPI-native one. Every server and client certificate k3s issues is valid for 365 days, and — critically — k3s checks on every restart whether any certificate is within 120 days of expiring and renews it automatically if so, no operator action required. There's also an explicit escape hatch, k3s certificate rotate, for forcing it on demand. That matters specifically because Cluster API's whole model is periodic reconciliation of a declared state — a control plane that self-heals its own certs on restart fits a reconcile loop that's already restarting and re-checking Machines on a schedule far better than one that needs a human to notice an expiry window and run a manual command.

Where the CAPI-native story for k3s gets rougher is the upgrade path itself: cluster-api-k3s's open issue #80 tracks that clusterctl alpha rollout restart kthreescontrolplane/<name> — the standard CAPI command for forcing a rolling control-plane restart — isn't supported yet. Upgrading a KThreesControlPlane's version field still triggers a rollout through the provider's own reconciler, but the generic clusterctl tooling every other CAPI control-plane provider (kubeadm included) supports for manually forcing that rollout doesn't work the same way here yet. That's a real, current gap between "the provider works" and "the provider matches the tooling every other Cluster API cluster in your fleet uses."

k0smotron's upgrade story inherits its hosted model's shape. Because the control plane runs as pods in the management cluster under K0smotronControlPlane, an upgrade is a Deployment image-tag bump reconciled by a controller already watching Kubernetes-native objects — no separate cloud-init re-run, no SSH back into a Hetzner box. That's a genuinely simpler reconcile surface for the control plane specifically. The tradeoff is that k0s's cert rotation isn't self-triggering on a restart schedule the way k3s's is; k0s follows upstream kubeadm-style rotation, which the kubelet handles automatically for its own client cert but which otherwise leans on the standard kubeadm certs renew pattern rather than a built-in "check and renew on every start" loop baked into the server process. For a hosted control plane living as a long-running Deployment rather than something that restarts on its own Machine-reconcile schedule, that's a materially bigger gap to actively manage.

The Verdict for a CAPH-Based Fleet

Neither distro is the wrong answer — they're the right answer to two different questions a platform needs to ask before picking one.

Choose k3s + cluster-api-k3s if tenant isolation means the control plane should be a real, billable, individually-failing Hetzner Machine — same failure domain as everything else in the fleet, same Machine-health-check and node-drain story, and a cert-rotation model that already assumes it's being restarted and re-checked on a schedule. Budget the small one-time cost of disabling Traefik and ServiceLB in every cluster template, and track issue #80 if your ops tooling leans on clusterctl alpha rollout restart for forcing upgrades — for now, trigger version bumps through the KThreesControlPlane spec directly rather than that command.

Choose k0s + k0smotron if tenant density matters more than physical isolation — every tenant's control plane as pods in a management cluster you're already running is real Hetzner spend you don't pay per-tenant, and upgrades become a Kubernetes-native Deployment rollout instead of a Machine-level operation. Budget for owning cert rotation actively rather than trusting a self-renewing default, and for deploying CNI/ingress/load-balancer explicitly on every cluster, since nothing ships pre-wired.

The size numbers that lead every other comparison of these two distros aren't wrong, they're just answering a question a fleet operator standing up node pools through Cluster API isn't actually asking. The one worth asking first is where you want the control plane to live, and everything else — the defaults you'll strip, the certs you'll need to watch, the upgrade command you can or can't run — follows from that one architectural choice.

Bex.co is the open-source, AI-native Render alternative — a Cluster API control plane provisioning real machines on Hetzner, not a hosted abstraction over infrastructure you never see. Star the repo on GitHub or deploy your first app today.


Sources:

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