Creating a Cluster
This page covers the full configuration reference for hetzner-k3s. For a quick start, see the Quick Start in the README or the Complete Tutorial.
Configuration File
hetzner-k3s uses a YAML configuration file. Below is a complete example with all options. Commented lines are optional:
---
hetzner_token: <your token>
cluster_name: test
kubeconfig_path: "./kubeconfig"
k3s_version: v1.32.0+k3s1
config_format_version: 1
networking:
ssh:
port: 22
use_agent: false # set to true if your key has a passphrase
use_private_ip: false # set to true to connect to nodes via their private IPs
public_key_path: "~/.ssh/id_ed25519.pub"
private_key_path: "~/.ssh/id_ed25519"
# existing_ssh_key_name: "my-existing-key" # optional: use an existing SSH key in Hetzner instead of creating a new one
allowed_networks:
ssh:
- 0.0.0.0/0
api: # this will firewall port 6443 on the nodes
- 0.0.0.0/0
# OPTIONAL: define extra inbound/outbound firewall rules.
# Each entry supports the following keys:
# description (string, optional)
# direction (in | out, default: in)
# protocol (tcp | udp | icmp | esp | gre, default: tcp)
# port (single port "80", port range "30000-32767", or "any") – only relevant for tcp/udp
# source_ips (array of CIDR blocks) – required when direction is in
# destination_ips (array of CIDR blocks) – required when direction is out
#
# IMPORTANT: Outbound traffic is allowed by default (implicit allow-all).
# If you add **any** outbound rule (direction: out), Hetzner Cloud switches
# the outbound chain to an implicit **deny-all**; only traffic matching your
# outbound rules will be permitted. Define outbound rules carefully to avoid
# accidentally blocking required egress (DNS, updates, etc.).
# NOTE: Hetzner Cloud Firewalls support **max 50 entries per firewall**. The built-
# in rules (SSH, ICMP, node-port ranges, etc.) use ~10 slots. If the sum of the
# default rules plus your custom ones exceeds 50, hetzner-k3s will abort with
# an error.
# custom_firewall_rules:
# - description: "Allow HTTP from any IPv4"
# direction: in
# protocol: tcp
# port: 80
# source_ips:
# - 0.0.0.0/0
# - description: "UDP game servers (outbound)"
# direction: out
# protocol: udp
# port: 60000-60100
# destination_ips:
# - 203.0.113.0/24
# node_port_firewall_enabled: true # optional: set false to disable NodePort firewall rules (TCP/UDP)
# node_port_range: "30000-32767" # optional: NodePort range to open on firewalls (TCP/UDP)
public_network:
ipv4: true
ipv6: true
# hetzner_ips_query_server_url: https://.. # for large clusters, see https://github.com/vitobotta/hetzner-k3s/blob/main/docs/Recommendations.md
# use_local_firewall: false # for large clusters, see https://github.com/vitobotta/hetzner-k3s/blob/main/docs/Recommendations.md
private_network:
enabled: true
subnet: 10.0.0.0/16
existing_network_name: ""
cni:
enabled: true
encryption: false
mode: flannel
cilium:
# Optional: specify a path to a custom values file for Cilium Helm chart
# When specified, this file will be used instead of the default values
# helm_values_path: "./cilium-values.yaml"
# chart_version: "v1.17.2"
# cluster_cidr: 10.244.0.0/16 # optional: a custom IPv4/IPv6 network CIDR to use for pod IPs
# service_cidr: 10.43.0.0/16 # optional: a custom IPv4/IPv6 network CIDR to use for service IPs. Warning, if you change this, you should also change cluster_dns!
# cluster_dns: 10.43.0.10 # optional: IPv4 Cluster IP for coredns service. Needs to be an address from the service_cidr range
# cluster_domain: cluster.local # optional: custom cluster domain
datastore:
mode: etcd # etcd (default) or external
external_datastore_endpoint: postgres://....
# etcd:
# # etcd snapshot configuration (optional)
# snapshot_retention: 24
# snapshot_schedule_cron: "0 * * * *"
#
# # S3 snapshot configuration (optional)
# s3_enabled: false
# s3_endpoint: "" # Can also be set with ETCD_S3_ENDPOINT environment variable
# s3_region: "" # Can also be set with ETCD_S3_REGION environment variable
# s3_bucket: "" # Can also be set with ETCD_S3_BUCKET environment variable
# s3_access_key: "" # Can also be set with ETCD_S3_ACCESS_KEY environment variable
# s3_secret_key: "" # Can also be set with ETCD_S3_SECRET_KEY environment variable
# s3_folder: ""
# s3_force_path_style: false
schedule_workloads_on_masters: false # set to true to allow pods to be scheduled on master nodes (useful for small clusters)
# image: rocky-9 # optional: default is ubuntu-24.04
# autoscaling_image: 103908130 # optional, defaults to the `image` setting
# snapshot_os: microos # optional: specified the os type when using a custom snapshot
masters_pool:
instance_type: cpx22
instance_count: 3 # for HA; you can also create a single master cluster for dev and testing (not recommended for production)
locations: # You can choose a single location for single master clusters or if you prefer to have all masters in the same location. For regional clusters (which are only available in the eu-central network zone), each master needs to be placed in a separate location.
- fsn1
- hel1
- nbg1
worker_node_pools:
- name: small-static
instance_type: cpx22
instance_count: 4
location: hel1
# image: debian-11
# labels: # Kubernetes labels to apply to nodes in this pool (for node selection in workloads)
# - key: purpose
# value: blah
# taints: # Kubernetes taints to apply to nodes in this pool (to repel pods unless they tolerate the taint)
# - key: something
# value: value1:NoSchedule
- name: medium-autoscaled
instance_type: cpx32
location: fsn1
autoscaling:
enabled: true
min_instances: 0
max_instances: 3
# addons:
# csi_driver:
# enabled: true # Hetzner CSI driver (default true). Set to false to skip installation.
# manifest_url: "https://raw.githubusercontent.com/hetznercloud/csi-driver/v2.21.2/deploy/kubernetes/hcloud-csi.yml"
# traefik:
# enabled: false # built-in Traefik ingress controller. Disabled by default.
# servicelb:
# enabled: false # built-in ServiceLB. Disabled by default.
# metrics_server:
# enabled: false # Kubernetes metrics-server addon. Disabled by default.
# cluster_autoscaler:
# enabled: true # Cluster Autoscaler addon (default true). Set to false to omit autoscaling.
# manifest_url: "https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/hetzner/examples/cluster-autoscaler-run-on-master.yaml"
# # local_manifest_path: "./cluster-autoscaler.yaml" # optional: use a local manifest file instead of fetching from manifest_url
# container_image_tag: "v1.35.0"
# scan_interval: "10s" # How often cluster is reevaluated for scale up or down
# scale_down_delay_after_add: "10m" # How long after scale up that scale down evaluation resumes
# scale_down_delay_after_delete: "10s" # How long after node deletion that scale down evaluation resumes
# scale_down_delay_after_failure: "3m" # How long after scale down failure that scale down evaluation resumes
# max_node_provision_time: "15m" # Maximum time CA waits for node to be provisioned
# cloud_controller_manager:
# enabled: true # Hetzner Cloud Controller Manager (default true). Disabling stops automatic LB provisioning for Service objects.
# manifest_url: "https://github.com/hetznercloud/hcloud-cloud-controller-manager/releases/download/v1.33.0/ccm-networks.yaml"
# system_upgrade_controller:
# enabled: true # System Upgrade Controller (default true). Set to false to omit autoscaling.
# deployment_manifest_url: "https://github.com/rancher/system-upgrade-controller/releases/download/v0.19.2/system-upgrade-controller.yaml"
# crd_manifest_url: "https://github.com/rancher/system-upgrade-controller/releases/download/v0.19.2/crd.yaml"
# embedded_registry_mirror:
# enabled: false # Enables fast p2p distribution of container images between nodes for faster pod startup. Check if your k3s version is compatible before enabling this option. You can find more information at https://docs.k3s.io/installation/registry-mirror
protect_against_deletion: true # prevents accidental deletion of the cluster with the "hetzner-k3s delete" command
create_load_balancer_for_the_kubernetes_api: false # creates a load balancer for HA API access; note: Hetzner firewalls can't yet restrict access to load balancers by IP
k3s_upgrade_concurrency: 1 # how many nodes to upgrade at the same time; increase for faster upgrades in large clusters, but higher values may impact availability
# additional_packages:
# - somepackage
# additional_pre_k3s_commands:
# - apt update
# - apt upgrade -y
# additional_post_k3s_commands:
# - apt autoremove -y
# For more advanced usage like resizing the root partition for use with Rook Ceph, see [Resizing root partition with additional post k3s commands](./Resizing_root_partition_with_post_create_commands.md)
# kube_api_server_args:
# - arg1
# - ...
# kube_scheduler_args:
# - arg1
# - ...
# kube_controller_manager_args:
# - arg1
# - ...
# kube_cloud_controller_manager_args:
# - arg1
# - ...
# kubelet_args:
# - arg1
# - ...
# kube_proxy_args:
# - arg1
# - ...
# api_server_hostname: k8s.example.com # optional: DNS for the k8s API LoadBalancer. After the script has run, create a DNS record with the address of the API LoadBalancer.
Most settings are straightforward and easy to understand. To see a list of available k3s releases, you can run the command hetzner-k3s releases.
If you prefer not to include the Hetzner token directly in the config file—perhaps for use with CI or to safely commit the config to a repository—you can use the HCLOUD_TOKEN environment variable instead. This variable takes precedence over the config file.
When setting masters_pool.instance_count, keep in mind that if you set it to 1, the tool will create a control plane that is not highly available. For production clusters, it's better to set this to a number greater than 1. To avoid split brain issues with etcd, this number should be odd, and 3 is the recommended value. Additionally, for production environments, it's a good idea to configure masters in different locations using the masters_pool.locations setting.
The datastore section configures how Kubernetes stores its cluster state. The default mode is etcd, which runs an embedded etcd cluster on your master nodes—this is the recommended option for most deployments. For very large clusters or special requirements, you can use external mode with an external datastore (etcd, PostgreSQL, or MySQL) by specifying the connection string in external_datastore_endpoint. The etcd mode also supports optional S3 backup configuration for disaster recovery.
You can define any number of worker node pools, either static or autoscaled, and create pools with nodes of different specifications to handle various workloads. Each pool can have optional labels and taints. Labels are key-value pairs that help you target specific nodes when scheduling workloads using node selectors or affinity rules. Taints prevent pods from being scheduled on certain nodes unless the pods explicitly tolerate the taint—useful for dedicating nodes to specific workloads or keeping certain nodes free for particular purposes.
Settings, such as additional_packages, additional_pre_k3s_commands, and additional_post_k3s_commands, can be specified at the root level of the configuration file or for each individual pool if different settings are needed. If these settings are configured at the pool level, they will override any settings defined at the root level.
additional_pre_k3s_commands: Commands executed before k3s installationadditional_post_k3s_commands: Commands executed after k3s is installed and configured
For an example of using additional_post_k3s_commands to resize the root partition for use with storage solutions like Rook Ceph, see Resizing root partition with additional post k3s commands.
The addons section controls which components hetzner-k3s installs automatically:
- csi_driver: The Hetzner CSI driver enables persistent volumes backed by Hetzner block storage. Enabled by default; disable if you're using alternative storage solutions like Rook Ceph or Longhorn.
- cloud_controller_manager: Integrates with Hetzner Cloud to provision load balancers automatically when you create Kubernetes Service objects of type LoadBalancer. Enabled by default.
- system_upgrade_controller: Enables zero-downtime rolling upgrades of k3s across your cluster. Enabled by default.
- cluster_autoscaler: Automatically scales worker node pools based on resource demands. Enabled by default when you have autoscaling pools defined.
- traefik: k3s's built-in ingress controller. Disabled by default; enable if you want a quick ingress solution without installing your own.
- servicelb: k3s's built-in load balancer for bare-metal environments. Disabled by default; typically not needed when using Hetzner's load balancers.
- metrics_server: Enables
kubectl topcommands for viewing resource usage. Disabled by default. - embedded_registry_mirror: Enables peer-to-peer distribution of container images between nodes for faster pod startup. Disabled by default; check k3s version compatibility before enabling.
The api_server_hostname setting is useful when you enable create_load_balancer_for_the_kubernetes_api. After the cluster is created, you can point this DNS name to the API load balancer's address, giving you a stable hostname for accessing the Kubernetes API.
Currently, Hetzner Cloud offers six locations: two in Germany (nbg1 in Nuremberg and fsn1 in Falkenstein), one in Finland (hel1 in Helsinki), two in the USA (ash in Ashburn, Virginia and hil in Hillsboro, Oregon), and one in Singapore (sin). Be aware that not all instance types are available in every location, so it’s a good idea to check the Hetzner site and their status page for details.
To explore the available instance types and their specifications, you can either check them manually when adding an instance within a project or run the following command with your Hetzner token:
To create the cluster run:
If you need to bypass the validation that your current IP is included in the allowed SSH/API networks, add --skip-current-ip-validation.
This process will take a few minutes, depending on how many master and worker nodes you have.
Disabling public IPs (IPv4 or IPv6 or both) on nodes
To improve security and save on IPv4 address costs, you can disable the public interface for all nodes by setting enable_public_net_ipv4: false and enable_public_net_ipv6: false. These settings are global and will apply to all master and worker nodes. If you disable public IPs, make sure to run hetzner-k3s from a machine that has access to the same private network as the nodes, either directly or through a VPN.
Additional networking setup is required via cloud-init, so it’s important that the machine you use to run hetzner-k3s has internet access and DNS configured correctly. Otherwise, the cluster creation process will get stuck after creating the nodes. For more details and instructions, you can refer to this discussion.
Using alternative OS images
By default, the image used for all nodes is ubuntu-24.04, but you can specify a different default image by using the root-level image config option. You can also set different images for different static node pools by using the image config option within each node pool. For example, if you have node pools with ARM instances, you can specify the correct OS image for ARM. To do this, set image to 103908130 with the specific image ID.
However, for autoscaling, there’s a current limitation in the Cluster Autoscaler for Hetzner. You can’t specify different images for each autoscaled pool yet. For now, if you want to use a different image for all autoscaling pools, you can set the autoscaling_image option to override the default image setting.
To see the list of available images, run the following:
export API_TOKEN=...
curl -H "Authorization: Bearer $API_TOKEN" 'https://api.hetzner.cloud/v1/images?per_page=100'
Besides the default OS images, you can also use a snapshot created from an existing instance. When using custom snapshots, make sure to specify the ID of the snapshot or image, not the description you assigned when creating the template instance.
I’ve tested snapshots with openSUSE MicroOS, but other options might work as well. You can easily create a MicroOS snapshot using this Terraform-based tool. The process only takes a few minutes. Once the snapshot is ready, you can use it with hetzner-k3s by setting the image configuration option to the ID of the snapshot and snapshot_os to microos.
Keeping a Project per Cluster
If you plan to create multiple clusters within the same project, refer to the section on Configuring Cluster-CIDR and Service-CIDR. Ensure that each cluster has its own unique Cluster-CIDR and Service-CIDR. Overlapping ranges will cause issues. However, I still recommend separating clusters into different projects. This makes it easier to clean up resources—if you want to delete a cluster, simply delete the entire project.
Configuring Cluster-CIDR and Service-CIDR
Cluster-CIDR and Service-CIDR define the IP ranges used for pods and services, respectively. In most cases, you won’t need to change these values. However, advanced setups might require adjustments to avoid network conflicts.
Changing the Cluster-CIDR (Pod IP Range):
To modify the Cluster-CIDR, uncomment or add the cluster_cidr option in your cluster configuration file and specify a valid CIDR notation for the network. Make sure this network is not a subnet of your private network.
Changing the Service-CIDR (Service IP Range):
To adjust the Service-CIDR, uncomment or add the service_cidr option in your configuration file and provide a valid CIDR notation. Again, ensure this network is not a subnet of your private network. Also, uncomment the cluster_dns option and provide a single IP address from the service_cidr range. This sets the IP address for the coredns service.
Sizing the Networks:
The networks you choose should have enough space for your expected number of pods and services. By default, /16 networks are used. Select an appropriate size, as changing the CIDR later is not supported.
Autoscaler Configuration
The cluster autoscaler automatically manages the number of worker nodes in your cluster based on resource demands. When you enable autoscaling for a worker node pool, you can also configure various timing parameters to fine-tune its behavior.
Basic Autoscaling Configuration
worker_node_pools:
- name: autoscaled-pool
instance_type: cpx32
location: fsn1
autoscaling:
enabled: true
min_instances: 1
max_instances: 10
Advanced Timing Configuration
You can customize the autoscaler's behavior with these optional parameters at the root level of your configuration:
cluster_autoscaler:
scan_interval: "2m" # How often cluster is reevaluated for scale up or down
scale_down_delay_after_add: "10m" # How long after scale up that scale down evaluation resumes
scale_down_delay_after_delete: "10s" # How long after node deletion that scale down evaluation resumes
scale_down_delay_after_failure: "15m" # How long after scale down failure that scale down evaluation resumes
max_node_provision_time: "15m" # Maximum time CA waits for node to be provisioned
worker_node_pools:
- name: autoscaled-pool
instance_type: cpx32
location: fsn1
autoscaling:
enabled: true
min_instances: 1
max_instances: 10
Parameter Descriptions
scan_interval: Controls how frequently the cluster autoscaler evaluates whether scaling is needed. Shorter intervals mean faster response to load changes but more API calls.-
Default:
10s -
scale_down_delay_after_add: Prevents the autoscaler from immediately scaling down after adding nodes. This helps avoid thrashing when workloads are still starting up. -
Default:
10m -
scale_down_delay_after_delete: Adds a delay before considering more scale-down operations after a node deletion. This ensures the cluster stabilizes before further changes. -
Default:
10s -
scale_down_delay_after_failure: When a scale-down operation fails, this parameter controls how long to wait before attempting another scale-down. -
Default:
3m -
max_node_provision_time: Sets the maximum time the autoscaler will wait for a new node to become ready. This is particularly useful for clusters with private networks where provisioning might take longer. - Default:
15m
These settings apply globally to all autoscaling worker node pools in your cluster.
External Node Pools
External node pools let you attach worker nodes from any provider (not just Hetzner Cloud) to your cluster. You provision and own these nodes; hetzner-k3s performs the same setup steps that cloud-init would on a Hetzner node (firewall, packages, k3s install) but via SSH instead.
Configuration
An external node pool uses instance_type: external and defines its nodes in the external section:
worker_node_pools:
- name: external-workers
instance_type: external
instance_count: 2
external:
provider: generic
nodes:
- host: 203.0.113.10
ssh_user: root
ssh_port: 22
ssh_private_key_path: ~/.ssh/external_node_key
manage_hostname: true
index: 1
- host: 198.51.100.20
ssh_user: ubuntu
ssh_port: 2222
ssh_private_key_path: ~/.ssh/external_node_key
manage_hostname: true
index: 2
For Hetzner Robot dedicated servers, use provider: robot at the pool level:
worker_node_pools:
- name: robot-workers
instance_type: external
instance_count: 2
external:
provider: robot
robot_user: your-robot-webservice-user
robot_password: your-robot-webservice-password
nodes:
- host: 203.0.113.30
robot_server_number: 123456
ssh_user: root
ssh_private_key_path: ~/.ssh/robot_node_key
manage_hostname: true
index: 1
- host: 203.0.113.31
robot_server_number: 123457
ssh_user: root
ssh_private_key_path: ~/.ssh/robot_node_key
manage_hostname: true
index: 2
If robot_user and robot_password are omitted, hetzner-k3s reads them from ROBOT_USER and ROBOT_PASSWORD.
Robot credentials are Robot Webservice credentials, created in the Robot UI under Settings -> Web service and app settings.
External provider modes
| Provider | Description |
|---|---|
generic |
Default. Use for external nodes from any provider, including Robot servers that should behave as unmanaged external workers. hetzner-k3s does not ask the Hetzner Cloud Controller Manager to initialize these nodes. |
robot |
Use for Hetzner Robot dedicated servers that should be known to the Hetzner Cloud Controller Manager as Robot nodes. Requires Robot Webservice credentials and robot_server_number for every node. |
Node properties
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
host |
String | yes | — | IP address of the external node (must be a valid IPv4 address, not a DNS hostname) |
ssh_user |
String | yes | — | SSH username for the node (e.g. root, ubuntu) |
ssh_port |
Int32 | no | 22 |
SSH port |
ssh_private_key_path |
String | yes | — | Path to the SSH private key (supports ~/ expansion) |
manage_hostname |
Bool | no | true |
If true, hetzner-k3s sets the node's hostname to match its naming convention |
index |
Int32 | yes | — | 1-based slot index; ties a node config to a specific hostname slot |
robot_server_number |
Int32 | yes for provider: robot |
— | Robot server number used for the hrobot:// provider ID and Robot API lookups |
The index field
The index property ties a node config entry to a specific hostname slot. When manage_hostname: true, the hostname is derived from the index (not the array position), so reordering the nodes array does not change hostnames. Indices must be:
- Unique within a pool (no duplicates)
- In the range
1..instance_count
For example, if instance_count: 3, valid indices are 1, 2, 3. The instance_count must equal the number of nodes in external.nodes.
Requirements
External nodes must:
- Run a Debian-based OS (Debian or Ubuntu). Package installation uses
apt-get. No other package managers (dnf, zypper, pacman) are supported. - Be accessible via SSH from the machine running hetzner-k3s, using the specified private key and user.
- Have root access — either the SSH user is
root, or the user has passwordless sudo.
Required cluster settings
External node pools require the following cluster-wide settings:
networking.private_network.enabled: false— external nodes cannot join a Hetzner private network.networking.public_network.use_local_firewall: true— the local firewall is deployed to each external node via SSH.networking.public_network.hetzner_ips_query_server_urlmust be set (required by the local firewall to fetch Hetzner node IPs).
Validation rules
The following rules are enforced at config validation time:
- Private network must be disabled.
- Local firewall must be enabled.
- No Hetzner-specific fields allowed in the pool (
image,autoscaling,grow_root_partition_automatically,legacy_instance_type). - Masters pool cannot use
instance_type: external. instance_countmust equal the number of nodes inexternal.nodes.- Node indices must be unique and in range
1..instance_count. - Node hosts must be unique within a pool.
- Robot pools must define Robot credentials and
robot_server_numberfor every node. - Robot server numbers must be unique across Robot external pools.
At runtime, before cluster creation begins, hetzner-k3s also validates:
- SSH accessibility — can it connect to each node?
- Root/sudo access — does the SSH user have root or passwordless sudo?
- Hostname conflicts — if
manage_hostname: false, does the existing hostname conflict with any hostname hetzner-k3s will generate? - Robot metadata — for
provider: robot,hostmust match the Robot server IP returned forrobot_server_number. Ifmanage_hostname: false, the Robot server name must be a valid Kubernetes node name and must match the node's OS hostname.
Setup behavior
When you run hetzner-k3s create, for each external node hetzner-k3s will:
- Set the hostname (if
manage_hostname: true). - Install packages (
fail2ban,wireguard, plus anyadditional_packages). - Configure the DNS resolver.
- Deploy the local firewall (rendered with the node's own SSH port).
- Run pre-k3s commands (
additional_pre_k3s_commands). - Install k3s worker and join it to the cluster.
- Run post-k3s commands (
additional_post_k3s_commands).
With provider: generic, external workers are not initialized by the Hetzner Cloud Controller Manager. hetzner-k3s installs them without kubelet's cloud-provider=external argument, and assigns a synthetic external://<public-ip> provider ID so the cloud node lifecycle controller will not delete them when they are temporarily NotReady.
With provider: robot, hetzner-k3s installs workers with kubelet's cloud-provider=external argument and assigns hrobot://<server-number> provider IDs. It also enables Robot support in the Hetzner Cloud Controller Manager manifest and adds the Robot credentials to the hcloud secret. If manage_hostname: true, hetzner-k3s updates the Robot server name through the Robot Webservice API before setting the OS hostname. If manage_hostname: false, the existing Robot server name and OS hostname must already match.
Robot nodes can be used as Hetzner Load Balancer IP targets through the Hetzner Cloud Controller Manager. Public-IP targets work with Robot API credentials. Private-IP targets require a Robot vSwitch/InternalIP setup and service annotation load-balancer.hetzner.cloud/use-private-ip: "true".
External nodes are labeled with hetzner-k3s.io/external=true and hetzner-k3s.io/external-provider=<provider>. The Hetzner CSI node driver is configured not to run on external nodes, because Hetzner Cloud Volumes can be attached only to Hetzner Cloud servers. When external pools are present, hetzner-k3s also sets the CSI controller's default volume location to the first master location so the controller does not depend on the Hetzner metadata service. Keep workloads that use the hcloud-volumes StorageClass on Hetzner Cloud nodes, or use a storage backend that supports your external nodes.
The hetzner-k3s delete command will clean up external nodes by uninstalling k3s, removing firewall files, and resetting iptables/ip6tables policies to accept traffic again via SSH.
hetzner-k3s run support
The run command detects external nodes by matching their IP against the external.nodes[].host entries. If a match is found, it uses the node's per-node SSH settings (key, user, port) instead of the cluster-wide SSH settings. This allows you to run commands and scripts on external nodes just like on Hetzner nodes.
Idempotency
The create command can be run multiple times with the same configuration without causing issues, as the process is idempotent. If the process gets stuck or encounters errors (e.g., due to Hetzner API unavailability or timeouts), you can stop the command and rerun it with the same configuration to continue where it left off. Note that the kubeconfig will be overwritten each time you rerun the command.
Limitations:
- Using a snapshot instead of a default image will take longer to create instances compared to regular images.
- The
networking.allowed_networks.apisetting specifies which networks can access the Kubernetes API. This works with both single-master and multi-master clusters, but only whencreate_load_balancer_for_the_kubernetes_apiis disabled. If the API load balancer is enabled, Hetzner's firewalls do not yet support load balancers, so the API would be exposed to the public internet regardless of the allowed networks configuration. - If you enable autoscaling for a nodepool, avoid changing this setting later, as it can cause issues with the autoscaler.
- Autoscaling is only supported with Ubuntu or other default images, not snapshots.
- If you already have SSH keys in your Hetzner project, it's best to use a different key for your cluster—unless there's already a key with the same name and fingerprint as the one in your config file. Hetzner doesn't allow two keys with the same fingerprint in one project. So if you've already added the key from your config but under a different name, Hetzner won't let you add it again. In that case, hetzner-k3s will skip creating the key and won't inject any SSH key into the cluster nodes. Without an SSH key, Hetzner sets up the nodes with password login instead. That means you'll get an email for each node with its root password. Managing several nodes this way can get tricky. To avoid this, you can either use a new keypair for your cluster, or set
existing_ssh_key_namein the SSH config to reference the existing key by name. When usingexisting_ssh_key_name, hetzner-k3s will use the specified key instead of creating a new one, and it won't delete the key when you delete the cluster. - SSH keys with passphrases can only be used if you set
networking.ssh.use_ssh_agenttotrueand use an SSH agent to access your key. For example, on macOS, you can start an agent like this: