Skip to content

Hetzner

The Hetzner provider maps Kyku abstract resources to Hetzner Cloud API services, plus Hetzner DNS and Hetzner S3-compatible object storage via separate APIs.

Terminal window
# Hetzner Cloud API (required)
export HCLOUD_TOKEN=your-cloud-token
# Hetzner DNS (optional, for DnsZone/DnsRecord)
export HETZNER_DNS_TOKEN=your-dns-token
# Hetzner S3 (optional, for Bucket)
export HETZNER_S3_REGION=fsn1
export HETZNER_S3_ACCESS_KEY=your-access-key
export HETZNER_S3_SECRET_KEY=your-secret-key

Provider HTTP used by this client goes through requestJson. Set KYKU_RECORD=1 / KYKU_REPLAY=1 to record or replay it; see HAR record and replay.

Service Backend Auth Method
Cloud API REST at api.hetzner.cloud/v1 Authorization: Bearer HCLOUD_TOKEN
DNS API REST at dns.hetzner.com/api/v1 Auth-API-Token header
S3-compat @aws-sdk/client-s3 w/ endpoint override AWS SigV4
Resource Hetzner Service Status
Vpc Network ✅
Subnet Network Subnet (synthetic) ✅
Vm Cloud Server ✅
SecurityGroup Firewall ✅
LoadBalancer Load Balancer ✅
TargetGroup LB Target Group ✅
Database — ❌
Identity — ❌
Role — ❌
SshKey SSH Key ✅
Bucket S3-compat (Object Storage) ✅
DnsZone DNS API ✅
DnsRecord DNS API ✅
Custom wired ✅
Abstract Hetzner
micro cax11
small cax21
medium cax31
large cax41
xlarge ccx13
2xlarge ccx33
4xlarge ccx43
8xlarge ccx53

Note: Older instance families (cx11, cx21, etc.) are deprecated in favor of Arm-based CAX and CCX series. The abstract mapping uses current-generation types.

Abstract Hetzner
ubuntu-22.04 ubuntu-22.04
ubuntu-24.04 ubuntu-24.04
debian-12 debian-12

Hetzner uses abstract location names. Regions map directly to locations:

Abstract Hetzner Location
us-east nbg1
us-west hil1
eu-central fsn1
eu-west nbg1

Hetzner locations: nbg1 (Nuremberg), fsn1 (Falkenstein), hil1 (Helsinki).

Every resource manager accepts a customConfig object merged directly into the underlying Hetzner API request — see Custom Config. @kykucloud/hetzner exports HetznerVpcCustomConfig, HetznerVmCustomConfig, HetznerSecurityGroupCustomConfig, HetznerLoadBalancerCustomConfig, and HetznerSshKeyCustomConfig, generated at build time from Hetzner’s official cloud.spec.json OpenAPI document (bun run generate:custom-config regenerates them; bun run check:custom-config verifies the committed output is current). Bucket (separate Object Storage API), DnsZone/DnsRecord (legacy DNS API, not in the cloud spec), KubernetesCluster (no native managed K8s), and TargetGroup (synthetic) don’t have — and can’t usefully have — a generated alias.

Vpc/Vm also route real updates through Hetzner’s action endpoints now: Network’s subnet/route/delete_protection via add_subnet/add_route/change_protection, Server’s backups/protection/placement_group/dns_ptr via their respective actions. One-shot commands (reboot, rescue, reset password, image creation) are deliberately not wired to customConfig — they aren’t declarative desired state.

Hetzner network zones use abstract names (eu-central) — not location names (fsn1, nbg1). Always use abstract zone identifiers.

Before attaching a server to a network, a subnet must exist in that network. Always set distributeAcrossAzs >= 1 on Vpc resources to auto-create subnets.

After creating a Load Balancer with network attachment, poll the private_net field via GET before adding targets. This can take up to 120s.

use_private_ip + Label Selectors Don’t Mix

Section titled “use_private_ip + Label Selectors Don’t Mix”

Setting use_private_ip: true alongside label_selector targets causes an API error. Omit use_private_ip when using label selectors.

// Add target with label selector
{ type: "label_selector", label_selector: { selector: "kyku-tg=tg-web" } }
// Remove from resources
{ remove_from: [{ type: "server", server: { id: 12345 } }] }

Firewall Destroy Fails with “Still In Use”

Section titled “Firewall Destroy Fails with “Still In Use””

Before deleting a firewall, PUT { applied_to: [] } to clear all label selectors. Otherwise the API rejects deletion.

Load Balancer POST Doesn’t Return private_net

Section titled “Load Balancer POST Doesn’t Return private_net”

The POST response for /load_balancers does not include the private_net field. Must poll GET to confirm network attachment.

Hetzner enforces name uniqueness across all resources in a project. Kyku checks by name before creating.

Max 63 chars ([a-z0-9-]). With the 9-char deploy prefix (8 hex + dash), effective limit is 54 chars for user-provided names.

The old cx11/cx21/cx31 series are deprecated in favor of cax11/cax21/cax31 (Arm). The abstract mapping already uses current-gen types.