Mix cloud and bare-metal pools
One cluster can run both bare-metal and cloud pools at the same time. A fixed bare-metal base carries the steady load, and an elastic cloud pool absorbs the spikes. This combination is often the cheapest and most reliable way to run production on Hetzner. The main decision is which workload belongs on which tier.
See Bare metal and cloud for how each backend provisions a machine.
Two classes, one cluster#
Add both classes as separate pools under spec.topology.workers.machineDeployments. Each pool has one class, one machine type, and its own labels and scaling.
| Class | Machines | Guide |
|---|---|---|
workeramd64baremetal | Dedicated Hetzner Robot servers you own, claimed from and released back to your host inventory | Add bare-metal servers |
workeramd64hcloud | Hetzner Cloud VMs, created and destroyed on demand | Add cloud servers |
"Created" versus "claimed" is the root difference between the two tiers. Cloud is elastic: Syself Autopilot calls the Hetzner API to make a new VM when you scale up, and deletes it when you scale down. Bare metal is a fixed pool: you already bought the servers, so scaling up claims a free HetznerBareMetalHost (the object that represents one physical server), and scaling down releases it for reuse.
Almost every other difference between the two tiers follows from that one fact: provisioning time, rollout order, and self-healing behavior.
Which workload belongs where#
| Bare metal | Cloud |
|---|---|
| Stateful workloads, databases, GPU and other compute-intensive work | Elastic and bursty work |
| Local NVMe (fast, tied to that one server) | Fast provisioning, ready in a few minutes |
| Highest raw CPU and memory per machine | Scale to zero when idle |
| No noisy-neighbor risk (a dedicated machine runs only your pods) | Pay per VM while running |
Bare metal keeps data on local disks that survive a reprovision of that same server, which is exactly what stateful services want. Cloud gives you machines in minutes and hands them back when the spike passes.
Bare-metal nodes also give you the highest raw CPU and memory per machine. That is why GPU workloads belong there too: there is no Hetzner Cloud GPU server type. See Add GPU nodes .
Storage differs by tier, with a different StorageClass for each:
| Tier | StorageClass | Backing |
|---|---|---|
| Cloud | standard (default) | Hetzner Cloud volumes, network storage, through the CSI (Container Storage Interface) driver |
| Bare metal | local-nvme, local-ssd, local-hdd | Local disks, through TopoLVM, a storage driver that turns a server's disks into Kubernetes volumes |
Note
Hetzner Cloud volumes attach to cloud servers only; there is no way to attach one to a bare-metal server. A pod claiming the standard class must therefore land on a cloud node, so pin volume-backed workloads to the cloud tier.
Note
TopoLVM is not part of the cluster stack. You install and operate it yourself. See Use local storage on bare metal . The standard cloud StorageClass needs no setup.
Warning
A local volume survives a reprovision of the same server: Syself rewrites the OS disk but leaves the data disks alone. It does not follow the workload to a different server. Run an application that replicates its own data across servers, such as CloudNativePG (a PostgreSQL operator) for a database, or back the data up elsewhere. See Run databases .
Steer each tier#
Label each pool so workloads land on the right hardware (see Label nodes and assign roles ), then select those labels from your pods with node affinity . Syself Autopilot also sets system labels you can select on directly, including autopilot.syself.com/machine-type (baremetal or hcloud) and instance.hetzner.cloud/is-root-server: "true" on bare metal.
Tip
Pool labels (metadata.labels on a machineDeployments entry) steer pods to nodes. They are a separate mechanism from workerHostSelectorBareMetal, which steers a bare-metal pool to specific HetznerBareMetalHost objects. By default, any free host can be claimed by any bare-metal pool. If you run more than one bare-metal pool, or share a namespace with another cluster, label your hosts and set workerHostSelectorBareMetal so each pool claims the right machines. See Add bare-metal servers .
Rollout differences#
The two classes roll out differently. This matters when an image update or a class change replaces nodes:
- Cloud rolls with
maxSurge=1: a new node comes up before the old one goes away, so capacity never dips. - Bare metal rolls with
maxSurge=0andmaxUnavailable=1: the old host is freed first, then reinstalled, one node at a time.
Warning
During a bare-metal rollout the pool runs at reduced capacity, because each host is taken down before its replacement is built. Size the pool so the remaining nodes can carry the load.
Self-healing also differs by tier. On cloud, Syself Autopilot first tries a reboot through the Hetzner API. If that fails, it deletes the VM and creates a new one.
On bare metal, Syself Autopilot first tries a hardware power-cycle through Hetzner Robot. If that fails, it releases the host back to the pool, so a healthy replacement can claim a free machine instead. A bare-metal worker that comes up cleanly is ready in a few minutes.
This only helps when another free host matches the pool's selector. If the selector is tight enough that no other host qualifies, the replacement re-claims the same machine, and a hardware fault returns after the reinstall. Self-healing cannot repair hardware; see Self-healing does not repair hardware for how to catch one.
A worked hybrid layout#
A steady bare-metal base carries the always-on load. A cloud pool on the autoscaler absorbs peaks. The bare-metal pool uses workerHostSelectorBareMetal so it only claims hosts you labelled for this cluster:
workers:
machineDeployments:
- class: workeramd64baremetal
name: md-base
replicas: 3
metadata:
labels:
node.cluster.x-k8s.io/tier: baseload
variables:
overrides:
- name: workerHostSelectorBareMetal
value:
matchLabels:
cluster: mycluster
- class: workeramd64hcloud
name: md-burst
metadata:
annotations:
cluster.x-k8s.io/cluster-api-autoscaler-node-group-min-size: "0"
cluster.x-k8s.io/cluster-api-autoscaler-node-group-max-size: "10"
labels:
node.cluster.x-k8s.io/tier: burst
variables:
overrides:
- name: workerMachineTypeHcloud
value: cpx42
Pin databases and stateful sets to tier: baseload, and let stateless, bursty work spill onto tier: burst. For the economics, see Size a cluster for cost .