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 is | A Docker container on your machine | A Hetzner Cloud VM, or a real dedicated Robot server |
| Intended use | Local development and CI testing only | Production clusters |
| Status | Reference implementation, lives in-tree at test/infrastructure/docker in kubernetes-sigs/cluster-api | Independently maintained, syself/cluster-api-provider-hetzner, GA since v1.0 (October 2024) |
| Current version line | Ships alongside core Cluster API releases | v1.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 CRDs | DockerCluster, DockerMachine | HetznerCluster, HCloudMachine, HetznerBareMetalHost, HetznerBareMetalMachine |
| Bootstrapping needs | kind, Docker socket mount, nothing else | Hetzner Robot web-service user, SSH keypair, two Kubernetes secrets (hetzner, robot-ssh), rootDeviceHints per host |
| Cloud provider integration | None — no LB, no volumes | hcloud-cloud-controller-manager for load balancers and networking |
| Production-suitable | No — Cluster API's own docs say so explicitly | Yes — 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:
rootDeviceHintsare 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 theHetznerBareMetalHostobject first and let the controller populatehardwareDetailsin 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-sshsecret. 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:
- 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. - Generate an SSH keypair and upload the public half to your hcloud project's Security → SSH Keys, using a name you'll reference later.
- Create the
hetznersecret in the management cluster holding your HCloud API token and Robot web-service credentials. - Create the
robot-sshsecret holding the public key, private key, and the exact SSH key name registered in Hetzner — matched, not just similar. - Only then does
clusterctl init --infrastructure hetznerhave 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
- Cluster API Provider Hetzner — GitHub
- What is the Cluster API Provider Hetzner and Compatible Versions — CAPH Docs
- HetznerBareMetalHost reference — CAPH Docs
- Configure Your Management Cluster for Handling Hetzner Bare Metal Servers — CAPH Docs
- Cluster API Provider Hetzner — Releases
- Kubernetes on Hetzner Using Bare Metal Servers as Nodes — Hetzner Community
- Managing Kubernetes on Hetzner with Cluster API — Hetzner Community
- Cluster API Book — Quick Start (CAPD)
- cluster-api/test/infrastructure/docker — GitHub