Karpenter cloud provider for Hetzner Cloud
  • Go 98.5%
  • Go Template 1.3%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
agent 00d19844af
Some checks failed
Cut release / changelog (push) Has been skipped
Cut release / test (push) Failing after 4s
Cut release / generate (push) Failing after 4s
Cut release / controllers (push) Failing after 4s
Cut release / chart (push) Successful in 9s
Roll release / plan (push) Failing after 3s
Roll release / image (push) Has been skipped
Roll release / chart (push) Has been skipped
Roll release / release (push) Has been skipped
Security / govulncheck (push) Successful in 1m47s
Security / govulncheck (shipped toolchain) (push) Successful in 1m48s
Cut release / lint (push) Successful in 3m54s
Cut release / ci (push) Failing after 0s
Cut release / cut (push) Has been skipped
fix(security): build with Go 1.27.3, cut 0.7.1
2026-10-22 22:21:20 +02:00
.github feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
charts/karpenter-provider-hetzner feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
cmd/controller feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
docs feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
examples feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
hack feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
pkg feat(metrics): publish server-type hourly prices (#23) 2026-10-22 19:51:38 +00:00
.gitignore chore: project scaffolding (go.mod, Makefile, Dockerfile) 2026-05-26 14:31:14 +02:00
.golangci.yml Provider production-hardening: deployable, correct, tested (#1) 2026-06-13 14:36:01 +02:00
CHANGELOG.md fix(security): build with Go 1.27.3, cut 0.7.1 2026-10-22 22:21:20 +02:00
CHANGELOG.upstream.md feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
CODE_OF_CONDUCT.md feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
CONTRIBUTING.md ci: split releases into cut and roll, as nats-operator does 2026-10-15 18:59:39 +02:00
Dockerfile fix(security): build with Go 1.27.3, cut 0.7.1 2026-10-22 22:21:20 +02:00
go.mod fix(security): build with Go 1.27.3, cut 0.7.1 2026-10-22 22:21:20 +02:00
go.sum feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
LICENSE Initial commit 2026-05-26 14:29:35 +02:00
license_test.go feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
README.md feat!: build the bootstrap around a generated k0s join, cut 0.6.0 2026-10-16 17:49:55 +02:00
renovate.json feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
SECURITY.md feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00
Taskfile.yaml feat!: release 0.1.0 as mikluko/karpenter-provider-hetzner 2026-10-15 18:18:00 +02:00

karpenter-provider-hetzner (fork)

CI Go Report Card License

A Karpenter cloud provider for Hetzner Cloud. It creates Hetzner Cloud servers as Kubernetes nodes for pending pods, choosing the cheapest server type that fits from Hetzner's live pricing, and removes them when they are no longer needed.

This is a fork of stubbi/karpenter-provider-hetzner, the original implementation, maintained independently since its 3.0.1 release. It was forked because contributions there stalled: no pull request was merged between 2026-09-07 and 2026-10-03, and eight of the nine contributed pull requests opened in that time waited 10 to 26 days without a review. With AI-assisted development and a provider this straightforward, carrying changes in a fork costs less than waiting for a maintainer to review them.

Status

The project is pre-1.0. It continues upstream karpenter-provider-hetzner 3.0.1; see the upstream changelog for its history. Release images are multi-arch, signed with cosign, and carry SLSA provenance and an SBOM. Pin a released version in production.

Features

  • All Hetzner Cloud server types: CX, CPX, CAX (Arm), and CCX.
  • Cheapest-first selection, priced from Hetzner's net hourly price.
  • Capacity awareness: an offering Hetzner reports as unavailable in a location is skipped for a few minutes, and Karpenter falls back to another.
  • Drift detection on image, network, firewall, and server type, which replaces the node.
  • Debian images, Hetzner's own or your labelled snapshots, resolved per architecture.
  • Placement groups that spread nodes across physical hosts.
  • Optional public IPv4 and IPv6, for clusters on a private network.
  • Several clusters per Hetzner project: every server carries its cluster's name, and a controller touches only its own.

How it works

A NodePool references an HCloudNodeClass. When pods are unschedulable, Karpenter asks this provider for the instance types it can offer, picks the cheapest compatible offering, and the provider creates the server. The server boots with a cloud-init document the provider builds from the node class's bootstrap: your parts around a join that installs k0s, with a join token made for that node alone.

Install

You need:

  • a k0s cluster on Hetzner Cloud, or another cluster whose nodes you join with a join of your own, as About node bootstrap describes;
  • the address of its API server, as a new server reaches it before it joins;
  • a Hetzner Cloud API token with read and write access.

To install:

  1. Store the token in a Secret:

    kubectl create secret generic hcloud-token \
      --namespace kube-system \
      --from-literal=token=<hcloud-token>
    
  2. Install the chart, with <cluster-name> naming this cluster:

    helm install karpenter-provider-hetzner \
      oci://ghcr.io/mikluko/charts/karpenter-provider-hetzner \
      --namespace kube-system \
      --set clusterName=<cluster-name> \
      --set apiServerURL=https://<api-server-address>:6443 \
      --set auth.secretRef.name=hcloud-token
    

clusterName and apiServerURL are required. The controller manages only servers labelled with clusterName, and new nodes join through apiServerURL.

The chart installs three CRDs from its crds/ directory: HCloudNodeClass, and the Karpenter NodePool and NodeClaim.

Upgrade

Helm installs the CRDs in crds/ and never updates them. When the release notes say the CRDs changed, apply them before upgrading the chart:

  1. Apply the CRDs of the release, with <version> naming it, for example v0.1.0:

    BASE=https://raw.githubusercontent.com/mikluko/karpenter-provider-hetzner/<version>/charts/karpenter-provider-hetzner/crds
    kubectl apply --server-side --force-conflicts \
      -f $BASE/karpenter.hetzner.cloud_hcloudnodeclasses.yaml \
      -f $BASE/karpenter.sh_nodeclaims.yaml \
      -f $BASE/karpenter.sh_nodepools.yaml
    

    --force-conflicts takes the fields over from Helm, which created the CRDs.

  2. Upgrade the chart, with <chart-version> naming the same release without the v, for example 0.1.0:

    helm upgrade karpenter-provider-hetzner \
      oci://ghcr.io/mikluko/charts/karpenter-provider-hetzner \
      --namespace kube-system --version <chart-version> --reuse-values
    

If a GitOps tool manages your CRDs, update them there instead; it reverts a manual apply.

Provision nodes

Create an HCloudNodeClass that says how servers are built, and a NodePool that says what Karpenter may create:

apiVersion: karpenter.hetzner.cloud/v1
kind: HCloudNodeClass
metadata:
  name: default
spec:
  locations: [nbg1, fsn1]
  networkID: 123456
  imageSelector:
    family: debian
    version: "13"
  enablePublicIPv4: false
  bootstrap:
    k0sVersion: v1.34.1+k0s.0
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      nodeClassRef:
        group: karpenter.hetzner.cloud
        kind: HCloudNodeClass
        name: default
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: [on-demand]
        - key: kubernetes.io/arch
          operator: In
          values: [amd64]
        - key: karpenter.hetzner.cloud/server-family
          operator: In
          values: [cx, cpx]
  limits:
    cpu: "100"
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized

k0sVersion names the k0s release the node installs, and should match the control plane's. Join Debian nodes to a k0s cluster walks through a node class, and About node bootstrap covers the parts around the join and how to replace it.

The examples/ directory holds the node class and node pool from that guide, and a pair of node pools for amd64 and arm64.

Every field, status condition, node label, and controller setting is listed in the reference.

Scheduling and cost

  • Hetzner sells no spot capacity, so pin karpenter.sh/capacity-type to on-demand in every NodePool. Without it, Karpenter requests spot for every consolidation replacement, no offering matches, and the replacement never launches. New nodes and other disruptions still work, which hides the fault.
  • Selection is cheapest-first. A NodePool that allows several architectures gets whichever is cheapest for the requested shape, which may be Arm (CAX). Allow arm64 only once your images are multi-arch.
  • Prices are Hetzner's net figures, before VAT, as on the invoice.
  • Hetzner bills a primary IPv4 separately. On a private-network cluster, set enablePublicIPv4: false to drop it.

Development

See CONTRIBUTING.md and the Code of Conduct.

Security

To report a vulnerability, follow SECURITY.md. Do not open a public issue.

License

Apache 2.0.