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.
Credentials
Section titled “Credentials”# 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=fsn1export HETZNER_S3_ACCESS_KEY=your-access-keyexport HETZNER_S3_SECRET_KEY=your-secret-keyProvider 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 |
Supported Resources
Section titled “Supported Resources”| 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 | ✅ |
Instance Type Mapping
Section titled “Instance Type Mapping”| 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.
Image Mapping
Section titled “Image Mapping”| Abstract | Hetzner |
|---|---|
ubuntu-22.04 |
ubuntu-22.04 |
ubuntu-24.04 |
ubuntu-24.04 |
debian-12 |
debian-12 |
Region Mapping
Section titled “Region Mapping”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).
Gotchas
Section titled “Gotchas”Custom Config
Section titled “Custom Config”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.
Network Zones are Abstract
Section titled “Network Zones are Abstract”Hetzner network zones use abstract names (eu-central) — not location names (fsn1, nbg1). Always use abstract zone identifiers.
Server Needs Subnet First
Section titled “Server Needs Subnet First”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.
LB Network Attachment is Async
Section titled “LB Network Attachment is Async”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.
Target Format
Section titled “Target Format”// 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.
Name Uniqueness
Section titled “Name Uniqueness”Hetzner enforces name uniqueness across all resources in a project. Kyku checks by name before creating.
Name Length
Section titled “Name Length”Max 63 chars ([a-z0-9-]). With the 9-char deploy prefix (8 hex + dash), effective limit is 54 chars for user-provided names.
Deprecated Server Types
Section titled “Deprecated Server Types”The old cx11/cx21/cx31 series are deprecated in favor of cax11/cax21/cax31 (Arm). The abstract mapping already uses current-gen types.