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.