Skip to content

Hetzner Provider

The Hetzner provider creates Kubernetes cluster nodes as Hetzner Cloud servers running Talos Linux. It provisions real cloud infrastructure with public IPs, private networking, load balancers, and persistent volumes — ideal for production-grade clusters and cloud testing.

The Hetzner provider is ideal when you:

  • Need production-grade Kubernetes on affordable European cloud infrastructure
  • Want real cloud load balancers and persistent volumes (not emulated locally)
  • Run performance or integration tests that require dedicated server resources
  • Deploy Talos Linux clusters with high-availability placement groups
  1. Hetzner Cloud account — Sign up at hetzner.com/cloud
  2. API token — Create one in the Hetzner Cloud Console → Project → Security → API Tokens (read/write permissions)
  3. Talos ISO or Schematic — Either a Talos Linux ISO must be available in your Hetzner Cloud project (x86 default: 122630, ARM: 122629), or a Talos factory schematic ID can be provided via spec.cluster.talos.schematicId so KSail builds the snapshot automatically. See Talos options.
  4. Docker — Required locally for ksail CLI operations

Export the Hetzner Cloud API token so KSail can manage servers:

Terminal window
export HCLOUD_TOKEN=your-hetzner-api-token

By default, KSail reads from HCLOUD_TOKEN. To use a different environment variable name, set spec.provider.hetzner.tokenEnvVar in ksail.yaml.

The Hetzner provider is configured through spec.provider.hetzner in ksail.yaml:

apiVersion: ksail.io/v1alpha1
kind: Cluster
metadata:
name: my-hetzner-cluster
spec:
cluster:
distribution: Talos
provider: Hetzner
controlPlanes: 1
workers: 2
talos:
# ISO image ID for Hetzner (default: 122630 for x86, use 122629 for ARM)
# Ignored when schematicId is set — KSail builds a snapshot instead.
iso: 122630
# Optional: Talos factory schematic ID. When set, KSail builds a Hetzner
# snapshot from the Talos factory URL and uses it instead of the ISO.
# Obtain a schematic ID from https://factory.talos.dev.
# schematicId: ""
provider:
hetzner:
# Server types (default: cx23 for both)
controlPlaneServerType: "cx23"
workerServerType: "cx23"
# Datacenter location (default: fsn1)
location: "fsn1"
# Private network settings
networkCidr: "10.0.0.0/16"
# networkName: "my-network" # defaults to <cluster>-network
# Optional: SSH key for server access (Talos API is primary)
# sshKeyName: "my-key"
# Optional: override the default env var name (default: HCLOUD_TOKEN)
# tokenEnvVar: "MY_CUSTOM_HCLOUD_TOKEN"
# Placement group settings for HA
placementGroupStrategy: "Spread" # or "None"
# placementGroup: "my-placement" # defaults to <cluster>-placement
# fallbackLocations: ["nbg1", "hel1"]
# placementGroupFallbackToNone: false
# Talos OS-level ingress firewall (default: Enabled — provides defense-in-depth)
# ingressFirewall: "Enabled" # or "Disabled"
# Maximum Hetzner servers for this cluster (default: 10)
# serverLimit: 10
FieldTypeDefaultDescription
hetzner.controlPlaneServerTypestringcx23Hetzner server type for control-plane nodes
hetzner.workerServerTypestringcx23Hetzner server type for worker nodes
hetzner.locationstringfsn1Datacenter location (fsn1, nbg1, hel1)
hetzner.networkNamestring<cluster>-networkPrivate network name
hetzner.networkCidrstring10.0.0.0/16Network CIDR block
hetzner.sshKeyNamestring(empty)SSH key for server access (optional)
hetzner.tokenEnvVarstringHCLOUD_TOKENEnvironment variable containing the API token
hetzner.placementGroupStrategystringSpreadSpread (HA) or None (no placement group)
hetzner.placementGroupstring<cluster>-placementPlacement group name
hetzner.fallbackLocationslist["nbg1", "hel1"]Alternative locations if primary is unavailable
hetzner.placementGroupFallbackToNoneboolfalseFall back to no placement group on capacity issues
hetzner.ingressFirewallstringEnabledTalos OS-level ingress firewall (Enabled or Disabled). Generates NetworkDefaultActionConfig + NetworkRuleConfig patches as defense-in-depth alongside the Hetzner Cloud Firewall.
hetzner.serverLimitint10Maximum total Hetzner servers for this cluster. Validated only when Hetzner node autoscaling is enabled — checks control-planes + workers + effective autoscaler capacity, where effective autoscaler capacity is sum(pool.max) or, when autoscaler.node.maxNodesTotal > 0, min(sum(pool.max), autoscaler.node.maxNodesTotal). Set to 0 to use the default of 10.
FieldTypeDefaultDescription
talos.isoint122630Hetzner Cloud ISO/image ID for Talos (use 122629 for ARM). Ignored when schematicId is set.
talos.schematicIdstring(empty)Talos factory schematic ID. When set, KSail automatically builds and manages a Hetzner snapshot image instead of booting from the ISO. spec.cluster.talos.version must also be set. Not supported for ARM64 server types (cax*). The snapshot is deleted when ksail cluster delete --delete-storage is run.

KSail supports node and pod autoscaling for Hetzner clusters via spec.cluster.autoscaler. Node pools are Hetzner-specific and map directly to the Kubernetes Cluster Autoscaler node group format.

spec:
cluster:
autoscaler:
node:
enabled: true
expander: LeastWaste # Price | LeastWaste | LeastNodes | Random
maxNodesTotal: 20 # 0 = no global cap (sum of pool maxes applies)
scaleDownUnneededTime: 10m
pools:
- name: workers-fsn1
serverType: cx23
location: fsn1
min: 1
max: 5
- name: workers-nbg1
serverType: cx33
location: nbg1
min: 0
max: 3
pod:
horizontal: Disabled # Enabled | Disabled (pod autoscaler setting; metrics-server is configured separately)
vertical: Disabled # Enabled | Disabled (reserved for future VPA support; not yet implemented)
provider:
hetzner:
serverLimit: 20 # Must be ≄ controlPlanes + workers + effective pool capacity (capped by maxNodesTotal when set)
FieldTypeDefaultDescription
autoscaler.node.enabledboolfalsetrue — defer node scaling to an external autoscaler; false — KSail manages node counts directly
autoscaler.node.expanderstringLeastWasteExpander strategy: Price, LeastWaste, LeastNodes, Random
autoscaler.node.maxNodesTotalint0Maximum total nodes across all pools. 0 disables the global cap.
autoscaler.node.scaleDownUnneededTimestring10mHow long a node must be unneeded before it is eligible for scale-down (e.g. 10m)
autoscaler.node.pools[].namestring—Unique pool name (DNS-1123 label, max 63 chars)
autoscaler.node.pools[].serverTypestring—Hetzner server type for this pool (e.g. cx23, cax11)
autoscaler.node.pools[].locationstring—Hetzner datacenter location for this pool (e.g. fsn1)
autoscaler.node.pools[].minint—Minimum nodes in this pool
autoscaler.node.pools[].maxint—Maximum nodes in this pool
autoscaler.pod.horizontalstringDisabledEnabled activates HPA support. Metrics-server is a prerequisite — configure it separately via spec.cluster.metricsServer.
autoscaler.pod.verticalstringDisabledReserved for future VPA support; not yet implemented.

Set HCLOUD_TOKEN as described in the Configuration section above, then verify it is exported in your shell.

Terminal window
ksail cluster init \
--name my-hetzner-cluster \
--distribution Talos \
--provider Hetzner \
--control-planes 1 \
--workers 2

This creates ksail.yaml and a talos/ directory for Talos configuration patches.

Terminal window
ksail cluster create

KSail creates Hetzner Cloud servers, boots them with Talos Linux, bootstraps Kubernetes, configures kubectl context, and installs the Hetzner Cloud Controller Manager and CSI driver.

Terminal window
ksail cluster info
kubectl get nodes -o wide
kubectl get pods -n kube-system
Terminal window
ksail cluster delete

KSail provisions Hetzner Cloud servers running Talos Linux, connected via a private network. The Hetzner Cloud Controller Manager provides native load balancer integration, and the Hetzner CSI Driver provisions persistent volumes backed by Hetzner Block Storage.

graph TB
    subgraph "Your Machine"
        KSAIL["ksail CLI"]
    end

    subgraph "Hetzner Cloud"
        API["Hetzner Cloud API"]
        NET["Private Network"]
        LB["Cloud Load Balancer"]

        subgraph "Servers"
            CP["Control Plane (Talos)"]
            W1["Worker Node 1 (Talos)"]
            W2["Worker Node 2 (Talos)"]
        end
    end

    KSAIL -->|"HCLOUD_TOKEN"| API
    API --> CP
    API --> W1
    API --> W2
    CP --- NET
    W1 --- NET
    W2 --- NET
    LB -->|"traffic"| W1
    LB -->|"traffic"| W2
    CP -.->|"kubeconfig"| KSAIL

When using the Hetzner provider, KSail automatically installs:

  • Hetzner Cloud Controller Manager — Provisions Hetzner Cloud Load Balancers for type: LoadBalancer services
  • Hetzner CSI Driver — Provisions Hetzner Block Storage volumes for PersistentVolumeClaim resources
  • Placement groups — Distributes servers across physical hosts for high availability (configurable)
  • Cluster Autoscaler — Installed automatically when autoscaler.node.enabled: true (or the deprecated nodeAutoscaling: Enabled). Scales node pools defined under autoscaler.node.pools based on pending pod demand.

KSail applies two independent firewall layers for Hetzner Talos clusters:

  1. Hetzner Cloud Firewall — Restricts external traffic at the network perimeter. KSail manages three rules: Talos API (50000/tcp), Kubernetes API (6443/tcp), and ICMP. Ports for cluster-internal communication (etcd, kubelet, trustd) are not exposed because nodes communicate over the private Hetzner Cloud Network.
  2. Talos OS-level ingress firewall — Defense-in-depth at the node level, enabled by default (spec.provider.hetzner.ingressFirewall: Enabled). Blocks all ingress by default; opens only the ports required for each node role (control plane vs worker), restricted to the cluster subnet where appropriate. Disable with ingressFirewall: Disabled if you manage node-level firewall rules yourself.

When spec.cluster.talos.schematicId is set, KSail manages a Hetzner Cloud snapshot image (instead of booting from the ISO) with the following lifecycle:

  • Create — ksail cluster create calls the Talos factory to download the raw disk image for the specified schematic and Talos version, uploads it to Hetzner Cloud as a snapshot, and labels it with ksail.io/talos-version, ksail.io/talos-schematic, and ksail.io/cluster. If a matching snapshot already exists, it is reused.
  • Reuse — Subsequent ksail cluster create calls find the existing snapshot by label and skip the upload step.
  • Delete — ksail cluster delete --delete-storage removes the snapshot along with the cluster. Without --delete-storage, the snapshot is retained (useful when recreating the cluster).
Terminal window
ksail cluster list
Terminal window
ksail cluster info

Displays Kubernetes control-plane and core service endpoints for the current context.

Terminal window
ksail cluster update

Applies in-place changes to components (CNI, GitOps engine, cert-manager, etc.). Node scaling changes for Talos are applied in-place. Changes to hetzner.location, hetzner.controlPlaneServerType, or hetzner.networkCidr require cluster recreation. See the Update Behavior table for details.

Terminal window
kubectl create deployment web --image=nginx --replicas=3
kubectl expose deployment web --port=80 --type=LoadBalancer
kubectl get svc web --watch

The type: LoadBalancer service provisions a real Hetzner Cloud Load Balancer with a public IP.

hcloud token is not set — The HCLOUD_TOKEN environment variable is missing or empty. Run echo $HCLOUD_TOKEN to check. Re-export if needed. If you use a custom variable name, verify it matches spec.provider.hetzner.tokenEnvVar in ksail.yaml.

Server creation fails with resource unavailability — The selected location may be out of capacity for the requested server type. Configure spec.provider.hetzner.fallbackLocations to try alternative datacenter locations automatically:

spec:
provider:
hetzner:
location: "fsn1"
fallbackLocations: ["nbg1", "hel1"]

Placement group errors — Hetzner limits spread placement groups to 10 servers per datacenter. Reduce your node count or set placementGroupStrategy: "None". For best-effort HA, set placementGroupFallbackToNone: true to fall back automatically when spread placement fails.

autoscaler configuration exceeds Hetzner server limit — The total node capacity (controlPlanes + workers + effectivePoolCapacity) exceeds hetzner.serverLimit. Increase serverLimit or reduce your pool max values (or lower maxNodesTotal to cap the effective pool capacity).

cloud provider requires an external registry — Hetzner Cloud servers cannot reach Docker-based local registries running on your machine. When spec.cluster.localRegistry is enabled, it must point to an internet-accessible registry (e.g., ghcr.io/myorg). KSail returns this error early if a non-external registry is configured.

context deadline exceeded or connection errors — Verify HCLOUD_TOKEN is valid and has read/write permissions. Check connectivity with curl -sI https://api.hetzner.cloud/v1/servers -H "Authorization: Bearer $HCLOUD_TOKEN".

Cluster deletion leaves orphaned resources — If ksail cluster delete is interrupted, manually clean up in the Hetzner Cloud Console: delete servers, load balancers, networks, and placement groups associated with your cluster name.