Skip to main content

CAPD vs CAPH: One Cluster API, From Your Laptop to Real Hetzner Bare Metal

9 min readDora NodaDora Noda
Share

Every Cluster API tutorial starts the same way: clusterctl init, a kind cluster, and a toy "workload cluster" made of Docker containers pretending to be nodes. What most of those tutorials never show is the other end of that same command — swap one infrastructure provider for another, and the identical YAML that spun up fake nodes on your laptop starts provisioning real dedicated servers with real disks, real IPMI, and a real invoice from Hetzner. CAPD and CAPH are that same contract at opposite extremes, and the distance between them is the whole reason a git-push PaaS can be built on Cluster API in the first place.

The Same API, Two Very Different Backends

CAPD (Cluster API Docker)CAPH (Cluster API Provider Hetzner)
What a "node" actually isA Docker container on your machineA Hetzner Cloud VM, or a real dedicated Robot server
Intended useLocal development and CI testing onlyProduction clusters
StatusReference implementation, lives in-tree at test/infrastructure/docker in kubernetes-sigs/cluster-apiIndependently maintained, syself/cluster-api-provider-hetzner, GA since v1.0 (October 2024)
Current version lineShips alongside core Cluster API releasesv1.2.x targets the v1beta2 contract, aligned with CAPI v1.11.x+; v1.1.x still supports the deprecated v1beta1 contract through CAPI v1.13.x
Key CRDsDockerCluster, DockerMachineHetznerCluster, HCloudMachine, HetznerBareMetalHost, HetznerBareMetalMachine
Bootstrapping needskind, Docker socket mount, nothing elseHetzner Robot web-service user, SSH keypair, two Kubernetes secrets (hetzner, robot-ssh), rootDeviceHints per host
Cloud provider integrationNone — no LB, no volumeshcloud-cloud-controller-manager for load balancers and networking
Production-suitableNo — Cluster API's own docs say so explicitlyYes — hundreds of organizations run it in production per Syself

That table is the entire argument in miniature. CAPD and CAPH both speak the exact same Cluster API resources — Cluster, KubeadmControlPlane, MachineDeployment — and the exact same clusterctl workflow. The only thing that changes between "toy cluster on my laptop" and "production fleet on owned Hetzner iron" is which InfrastructureProvider you initialized and which flavor template you applied. That's the point of Cluster API existing at all: one declarative contract, pluggable backends.

CAPD: Real Reconciliation, Fake Hardware

CAPD is the Cluster API project's own reference infrastructure provider, living directly in the kubernetes-sigs/cluster-api repo rather than as a separate project. It's built to validate Cluster API's core controllers, not to run anything anyone depends on — the docs are explicit that it's for local development and CI, full stop.

What makes CAPD useful is what it doesn't fake: the control-plane reconciliation loop, the bootstrap provider (kubeadm), and the Cluster/Machine/MachineDeployment object graph are all the real Cluster API code path. Only the "server" is fake — a DockerMachine is backed by a container running a kind node image, connected to the host's Docker daemon (which is why the standard setup mounts /var/run/docker.sock into the kind bootstrap cluster). Scale a MachineDeployment from 3 replicas to 5, and you watch the exact same controller logic that would provision two more bare-metal servers instead spin up two more containers. You can kubectl delete a "node" mid-reconciliation and watch the same self-healing behavior a production fleet relies on — for free, on a laptop, in seconds instead of the minutes a real Hetzner provision takes.

CAPH: The Same Contract, Real Servers

CAPH is a different kind of project entirely: an independently maintained, GA (since v1.0, October 2024), production-grade provider built by Syself and the community specifically to run Kubernetes on Hetzner's infrastructure — both Hetzner Cloud VMs and, more distinctively, real dedicated Robot servers.

The Robot side is where CAPH earns the "bare metal" half of its name, and it's also where the CRD graph gets more interesting than CAPD's two types. A HetznerBareMetalHost has a one-to-one relationship with a physical dedicated server — you create it by hand, pointing at a serverID from your Hetzner Robot dashboard — but the host object itself isn't scoped to any one cluster; the same host can sit unclaimed until a HetznerBareMetalMachine selects it, at which point provisioning kicks off and a consumerRef tracks the claim. Delete the machine, and the host cycles back to "available" instead of being destroyed, since it's a physical box, not a VM you can just terminate.

Two details in that lifecycle matter more than they look:

  • rootDeviceHints are not optional. Bare-metal servers ship with more than one disk, and CAPH has no way to guess which one should hold the OS. Skip this, and the server never joins the cluster — it just sits there. If you don't know a disk's WWN ahead of time, the documented trick is to create the HetznerBareMetalHost object first and let the controller populate hardwareDetails in its status, then copy the WWN back into the spec.
  • SSH key names must match exactly between what's registered in Hetzner Robot and what's referenced in the robot-ssh secret. A mismatch doesn't error loudly — it just leaves the controller unable to reach the machine it's trying to provision.

Neither gotcha exists in CAPD, because CAPD never has to reason about which physical disk to partition or how to reach a machine that isn't a container on the same Docker network.

Bootstrapping the Management Cluster: The Part the Diagrams Skip

Every Cluster API setup needs a management cluster — the place where the Cluster/Machine controllers themselves run, reconciling against whichever infrastructure provider you've installed. For CAPD, that management cluster is disposable: spin up a kind cluster, clusterctl init --infrastructure docker, done. For CAPH running real Robot hardware, the same step has real prerequisites attached:

  1. Create a web-service user in Hetzner Robot (robot.your-server.de/preferences) — this is a separate credential from your normal Robot login, scoped for API access.
  2. Generate an SSH keypair and upload the public half to your hcloud project's Security → SSH Keys, using a name you'll reference later.
  3. Create the hetzner secret in the management cluster holding your HCloud API token and Robot web-service credentials.
  4. Create the robot-ssh secret holding the public key, private key, and the exact SSH key name registered in Hetzner — matched, not just similar.
  5. Only then does clusterctl init --infrastructure hetzner have anything to reconcile against.

None of that is exotic — it's the standard shape of "give a controller a credential and a way to reach the thing it manages" — but it's five steps CAPD skips entirely, and every one of them is a place a first bare-metal cluster silently fails to provision if missed.

The Gotcha That Made It Into the Changelog

The clearest illustration that CAPH manages real infrastructure, not an abstraction over it, is a regression that had nothing to do with CAPH's own code. On July 9, 2025, a change on Hetzner's side broke compatibility with cluster-api-provider-hetzner in a specific and painful way: control-plane node deletions started failing, leaving resources stuck mid-teardown. A CAPD "node" is a container — docker rm always works, because Docker's API doesn't get a silent upstream personality change. A CAPH-managed control-plane node is a real server behind a real vendor API, and when that API's deletion semantics shifted, CAPH's reconciliation loop broke until a patched release shipped. Restarting the controller offered temporary relief; the real fix required upgrading.

This is the tradeoff CAPH is explicitly making and CAPD explicitly isn't: real infrastructure comes with a real upstream surface that can change under you, and a provider mature enough to run production fleets has to absorb that risk on behalf of everyone using it — which is exactly what the CAPH maintainers did here.

Why "Your Machines, One Kubernetes API" Is the Point

The reason this pairing matters beyond Hetzner specifically is that it's the shape every Cluster-API-based platform needs to work at all: a fast, disposable, zero-cost-per-iteration provider for development and CI, and a slow, stateful, real-money provider for production — both speaking the identical Cluster/MachineDeployment/KubeadmControlPlane object graph. A platform engineer building CI for a Cluster-API-based PaaS doesn't need a Hetzner account to test that a MachineHealthCheck correctly triggers remediation; they need CAPD. The same YAML, the same controllers, the same test assertions run against CAPH-managed bare metal in production, with only the InfrastructureProvider swapped.

That's the foundation a self-hosted, git-push PaaS is built on: one declarative API surface for "here is the fleet of machines this platform runs on," whether those machines are disposable test containers or dedicated servers with their own IPMI and their own vendor invoice. An AI agent asking "is my fleet healthy" or "provision me a replacement node" is asking the same Cluster API question regardless of which provider answers it — which is exactly the property that makes Cluster API, not a bespoke SSH-based fleet manager, the right foundation for infrastructure an agent is expected to reason about and operate.

Bex.co is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own, backed by Cluster API's declarative fleet management instead of a hand-managed server pool. 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