A Karpenter cloud provider for Hetzner Cloud. It provisions, bin-packs, and autoscales Hetzner Cloud servers as Kubernetes nodes, picking the cost-optimal server type for pending pods from Hetzner's real-time pricing.
Stable (v1.0.0). The full CloudProvider surface is implemented with unit and controller test coverage, and the provision → join → drift → consolidation lifecycle is validated end-to-end against a live Talos cluster (happy, drift, consolidation, fallback, and invalid-nodeclass scenarios). Releases are multi-arch, cosign-signed, and ship SLSA provenance + an SBOM. Pin a released version tag in production.
- All Hetzner Cloud server types — CX, CPX, CAX, CCX, including ARM (CAX).
- Cost-optimal scheduling — offerings are priced from Hetzner's live per-hour net price, so Karpenter bin-packs onto the cheapest type that fits.
- Capacity-aware — when Hetzner reports a type as unavailable in a location, that offering is skipped for a short window so scheduling falls back to an alternative instead of looping on a sold-out combination.
- Drift detection — image, network, firewall, and server-type drift trigger node replacement.
- Talos Linux and Ubuntu images, resolved per architecture.
- Placement groups for spreading nodes across physical hosts.
- Cost controls — opt out of the billed public IPv4 (and/or IPv6) per node class for private-network clusters.
- Multi-cluster safe — every managed server is tagged with the cluster name, so several clusters can share one Hetzner project without touching each other's nodes.
┌──────────────────────────────────────────────────────────┐
│ Karpenter core (provisioning, disruption, NodeClaim GC) │
└───────────────┬────────────────────────────────────────────┘
│ CloudProvider interface
┌───────────────▼────────────────────────────────────────────┐
│ karpenter-provider-hetzner │
│ • instance — hcloud server create/delete/get/list │
│ • instancetype — server types → priced InstanceTypes │
│ • imagefamily — resolve Talos/Ubuntu images per arch │
│ • nodeclass ctrl— validate HCloudNodeClass, set Ready │
└───────────────┬────────────────────────────────────────────┘
│ hcloud API
┌───────▼────────┐
│ Hetzner Cloud │
└─────────────────┘
A NodePool references an HCloudNodeClass. When pods are unschedulable, Karpenter asks this provider for instance types, picks the cheapest compatible offering, creates the server, and the node joins the cluster.
You need a Hetzner Cloud API token with read/write access and a Kubernetes cluster on Hetzner (the hcloud Cloud Controller Manager should set node provider IDs as hcloud://<id>).
kubectl create secret generic hcloud-token \
--namespace kube-system \
--from-literal=token=$HCLOUD_TOKEN
helm install karpenter-provider-hetzner \
oci://ghcr.io/paperclipinc/charts/karpenter-provider-hetzner \
--namespace kube-system \
--set clusterName=my-cluster \
--set auth.secretRef.name=hcloud-tokenclusterName is required — it scopes which servers this controller manages. The controller fails fast if it is unset.
The CRD ships in the chart's crds/ directory and is installed automatically by Helm.
Create an HCloudNodeClass describing how nodes are built, and a NodePool describing what Karpenter may provision:
apiVersion: karpenter.hetzner.cloud/v1
kind: HCloudNodeClass
metadata:
name: default
spec:
locations: [nbg1, fsn1] # Hetzner locations to schedule into
networkID: 123456 # private network the nodes join
imageSelector:
family: talos # talos | ubuntu
version: "v1.9" # optional substring match
firewallIDs: [987654] # optional
sshKeyIDs: [42] # optional
placementGroupStrategy: spread # spread | none (default spread)
enablePublicIPv4: false # default true; false saves the primary-IPv4 charge
userData: | # cloud-init / Talos machine config (or use userDataSecretRef to source from a Secret)
...
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.hetzner.cloud
kind: HCloudNodeClass
name: default
requirements:
- key: kubernetes.io/arch
operator: In
values: [amd64, arm64]
- key: karpenter.hetzner.cloud/server-family
operator: In
values: [cax, cpx]
limits:
cpu: "100"
disruption:
consolidationPolicy: WhenEmptyOrUnderutilizedProvisioned nodes carry, in addition to the well-known Karpenter labels:
| Label | Example | Meaning |
|---|---|---|
topology.kubernetes.io/zone |
nbg1 |
Hetzner location |
node.kubernetes.io/instance-type |
cax21 |
Hetzner server type |
karpenter.hetzner.cloud/server-family |
cax |
Type family (cx/cpx/cax/ccx) |
karpenter.hetzner.cloud/cpu-type |
shared |
shared or dedicated |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
locations |
[]string |
yes | — | Hetzner locations (min 1) |
networkID |
int64 |
yes | — | Private network ID nodes attach to |
imageSelector.family |
talos|ubuntu |
yes | — | OS image family |
imageSelector.version |
string |
no | newest | Version substring to match against the image description |
imageSelector.selector |
map[string]string |
no | — | hcloud label filter applied when listing images (e.g. {"caph-image-name": "talos-v1.9.5-gvisor"}). Prefer this over version to pin an exact snapshot (version + baked extensions). All labels must match; the provider guards against provisioning a node whose resolved image arch does not match the server type. |
firewallIDs |
[]int64 |
no | — | Firewalls to attach |
sshKeyIDs |
[]int64 |
no | — | SSH keys to install |
placementGroupStrategy |
spread|none |
no | spread |
Placement-group behavior. spread distributes nodes across physical hosts; none disables placement groups. |
enablePublicIPv4 |
*bool |
no | true |
Attach a public IPv4 (billed separately by Hetzner). Set false on private-network clusters to drop the charge. |
enablePublicIPv6 |
*bool |
no | true |
Attach a public IPv6. |
labels |
map[string]string |
no | — | Extra hcloud labels on the Hetzner server (useful for cost attribution or firewall label-selectors) |
userData |
string |
no | — | Inline cloud-init / Talos machine config. Overridden by userDataSecretRef when both are set. |
userDataSecretRef |
object {namespace, name, key} |
no | — | Source userData from a Secret instead of inline. The Secret is read at server-create time; its value never appears in the NodeClass spec or git. Takes precedence over userData. |
Status exposes conditions (ImagesReady, NetworkReady, ResourcesReady, UserDataReady, aggregated into Ready) and resolvedImages (image ID per architecture).
Ready-to-apply examples live in the examples/ directory. Each
file is multi-document YAML (NodeClass + NodePool in one file) with inline
comments explaining every field.
| File | Description |
|---|---|
examples/talos-nodeclass.yaml |
Talos Linux, private-network cluster, image pinned via label selector, machineconfig from a Secret |
examples/ubuntu-nodeclass.yaml |
Ubuntu 24.04, kubeadm join via inline cloud-init userData |
examples/k3s-nodeclass.yaml |
k3s agent join via cloud-init on an Ubuntu image (for k3s-based clusters) |
examples/nodepool-multiarch.yaml |
Multi-arch pattern: one NodeClass, two NodePools (amd64 CCX + arm64 CAX) |
- Talos bootstrap guide — obtaining the worker machineconfig, pinning images, and verifying node join.
- Ubuntu bootstrap recipe — kubeadm join via cloud-init, keeping tokens out of git, trade-offs vs Talos.
- k3s bootstrap recipe — k3s agent join via cloud-init, using the hcloud CCM for providerID; the natural path for k3s installers (hetzner-k3s, kube-hetzner).
| Env var | Required | Description |
|---|---|---|
HCLOUD_TOKEN |
yes | Hetzner Cloud API token |
CLUSTER_NAME |
yes | Cluster identifier; scopes managed servers |
METRICS_PORT |
no (8080) | Prometheus metrics port |
HEALTH_PROBE_PORT |
no (8081) | Health/readiness probe port |
- Pricing uses the net hourly figure, so relative comparisons match your Hetzner invoice (before VAT).
- Hetzner bills the primary IPv4 separately. On private-network clusters, set
enablePublicIPv4: falseto drop it. - ARM (CAX) types are typically the best price/performance; constrain
kubernetes.io/archorserver-familyin the NodePool to steer Karpenter.
See CONTRIBUTING.md. Quick start:
make test # unit + controller tests (race)
make lint # golangci-lint
make generate # regenerate CRD + deepcopy
make build # build the controller binaryContributions are welcome — please read CONTRIBUTING.md and the Code of Conduct.
To report a vulnerability, see SECURITY.md. Please do not open public issues for security reports.