Skip to content

First Cluster

A detailed walkthrough of the cluster.lok8s.yaml configuration file.

Prerequisites

  • lok8s CLI installed
  • A Hetzner Cloud account with an API token
  • An SSH key pair (Ed25519 recommended)
  • A domain you control: the API endpoint and certificate names derive from it (set as spec.cluster.domain; example.com below is a placeholder)

Full configuration reference

Show the full cluster.lok8s.yaml
yaml
# clusters/example.com/cluster.lok8s.yaml
apiVersion: cluster.lok8s.dev/v1beta1
kind: KubeOne
metadata:
  name: my-cluster
spec:
  kubernetes:
    version: "v1.35.5"
  cluster:
    # REQUIRED: a domain you control; used for ingress and cert names
    domain: example.com
    # default namespace for workloads
    namespace: default
  provider:
    name: hetzner
    # The machines, in Hetzner's own terms. Each `server` entry becomes one
    # `hcloud server create`; its keys are hcloud flags. Put the same content
    # in a separate file and point `configRef: hetzner.json` at it if you
    # prefer a shorter spec.
    config:
      cluster_name: my-cluster
      sshUser: root
      sshPrivateKey: ~/.ssh/id_ed25519
      sshPublicKey: ~/.ssh/id_ed25519.pub
      ssh-key:
        - name: my-cluster
          public-key-from-file: ~/.ssh/id_ed25519.pub
      network:
        - name: my-cluster
          # private network range
          ip-range: 10.0.0.0/16
          "#subnets":
            - network-zone: eu-central
              type: cloud
              ip-range: 10.0.0.0/24
      server:
        # 3 control planes for HA; 1 is enough for dev
        - name: cp-1
          type: cx33
          image: ubuntu-24.04
          # fsn1, nbg1, or hel1
          location: fsn1
          ssh-key: [0]
          network: 0
          label: lok8s.dev/cluster=my-cluster,lok8s.dev/role=control-plane
        - name: cp-2
          type: cx33
          image: ubuntu-24.04
          location: fsn1
          ssh-key: [0]
          network: 0
          label: lok8s.dev/cluster=my-cluster,lok8s.dev/role=control-plane
        - name: cp-3
          type: cx33
          image: ubuntu-24.04
          location: fsn1
          ssh-key: [0]
          network: 0
          label: lok8s.dev/cluster=my-cluster,lok8s.dev/role=control-plane
        - name: worker-1
          type: cpx31
          image: ubuntu-24.04
          location: fsn1
          ssh-key: [0]
          network: 0
          label: lok8s.dev/cluster=my-cluster,lok8s.dev/role=worker
        - name: worker-2
          type: cpx31
          image: ubuntu-24.04
          location: fsn1
          ssh-key: [0]
          network: 0
          label: lok8s.dev/cluster=my-cluster,lok8s.dev/role=worker
  # cluster-infra addons, applied in order
  bootstrap:
    # CNI (the default if omitted)
    - cilium
    # Hetzner cloud-controller-manager
    - ccm
    - cert-manager
    - monitoring
  kubehz:
    # self (you run everything) | hosted (kubehz runs the control plane)
    # | shared (a Space on a shared control plane, kind: Kubehz)
    hosting: self
    # none | registered | managed (Supporter+)
    access: registered
    # cronjob (read-only heartbeat) | operator (managed tier, live view)
    agent: cronjob
    apiUrl: https://api.kubehz.cloud

Key sections

spec.kubernetes

Set the Kubernetes version. kubehz offers a curated list, each version pinned to a patch release: today v1.34.8 and v1.35.5 (the default). A version is withdrawn as soon as upstream stops patching its minor.

spec.provider

Who provides the machines. name: hetzner selects Hetzner Cloud, the one implemented provider. config (or configRef, a path to a JSON or YAML file next to the spec) is the provider descriptor: the SSH access lok8s uses (sshUser, sshPrivateKey, sshPublicKey), and one list per Hetzner resource (ssh-key, network, server, and optionally volume and load-balancer). Each entry becomes one hcloud <resource> create, and its keys are that command’s flags. A server’s ssh-key: [0] and network: 0 refer to entries of those lists by index.

The lok8s.dev/role label decides what a server becomes: control-plane or worker. Three control planes give you HA; one is enough for development. type is a Hetzner server type, location the datacenter.

Bare metal. A Hetzner dedicated server joins the same descriptor as a #cloud.root entry (a Robot server that lok8s reimages and enrolls). See the lok8s provider reference for the shape.

Deprecated shape. Older specs carried spec.ssh, spec.controlPlane and spec.workers. lok8s still reads them as fallbacks, but they should not be used in new specs: the provider descriptor owns SSH access and node topology.

spec.bootstrap

An ordered list of cluster-infra addons (CNI, CCM, cert-manager, monitoring, …) applied at provision time, before any workloads land. Each entry is applied and waited on before the next. Bare names resolve to lok8s framework addons; ./path entries point at your own kustomize directories. If omitted, it defaults to cilium: every cluster needs a CNI. See the lok8s addons guide for the full list.

spec.kubehz

Optional kubehz platform integration. hosting says who runs the control plane: self (you, on your own account), hosted (kubehz, see Hosted) or shared (a Space on a control plane kubehz shares between customers; this one needs kind: Kubehz). access says what kubehz sees: none, registered (read-only health) or managed (adds the management features: healing policies, capacity watches, desired-state management; requires a Supporter subscription or higher). Acting is pull-based: the in-cluster agent fetches desired state from the platform and applies it with your cluster’s own credentials; kubehz never holds inbound access.

The other keys are optional:

KeyMeaning
agentWhich in-cluster agent owns the heartbeat: cronjob (the read-only default) or operator (the managed tier’s live view).
apiUrlThe kubehz API, HTTPS only. Required whenever access is not none, and for hosted and shared.
connectHcloudTokentrue hands the platform your HCLOUD_TOKEN at registration, over HTTPS only, so a hosted cluster can provision worker pools. Off by default.
upgradeschannel: none / patch / minor (how far the platform may upgrade unasked, default patch) and defer: window / immediate.
maintenanceWindowWhen platform-driven work may run: daysOfWeek, startTime, durationMinutes, timezone, and exclusions (dates or ranges that freeze everything).
spacehosting: shared only: the Space’s slug, display name, and the nodes to mint join tickets for on provision.

See How it works for the full model and the trust boundary, and Registration to turn on visibility.

Save the file

lo keeps one folder per cluster under clusters/, named after the domain, and reads cluster.lok8s.yaml from there. Create the folder, save the file, then mark the domain active:

bash
mkdir -p clusters/example.com
# paste the reference above into clusters/example.com/cluster.lok8s.yaml
lo use example.com

lo use records the active domain in clusters/.active; --domain <domain> or DOMAIN_NAME override it for a single command.

Edit these before provisioning. The reference ships placeholders, not defaults:

FieldPlaceholderWhat it must be
metadata.namemy-clusteryour cluster’s name
spec.cluster.domain and the folderexample.coma domain you control (it is used for the API endpoint)
the SSH paths in provider.config~/.ssh/id_ed25519[.pub]your key’s real path, only if it is not the default Ed25519 name

Give lok8s your Hetzner token

lo provision creates real, billable infrastructure in your Hetzner account and authenticates with the API token from the prerequisites. Export it in the same shell you are about to provision from. It is read from the environment, never from the config file, so it never lands in git:

bash
export HCLOUD_TOKEN="<your-token>"

Provision your cluster

Run this from your project directory, with the domain active:

bash
lo provision

Provisioning typically takes 10–15 minutes. The kubeconfig is written to .kubeconfig/my-cluster.yaml in your project, named after metadata.name. Your ~/.kube/config is never touched.

The reference config above asks Hetzner for five servers (3 control plane and 2 workers), billed hourly at Hetzner’s published prices for those server types. Nothing here is one-way: lo destroy removes every server lok8s created, whenever you want.

Verify

kubectl still points at whatever context it used before, so select the new cluster explicitly; otherwise get nodes answers for your previous one:

bash
export KUBECONFIG=.kubeconfig/my-cluster.yaml
kubectl get nodes
# (AGE trimmed — STATUS is what to check)
# NAME       STATUS   ROLES           VERSION
# cp-1       Ready    control-plane   v1.35.5
# cp-2       Ready    control-plane   v1.35.5
# cp-3       Ready    control-plane   v1.35.5
# worker-1   Ready    <none>          v1.35.5
# worker-2   Ready    <none>          v1.35.5

kubectl get pods -A

# Lists ONLY pods that have not settled. On a healthy cluster it prints nothing:
kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded
# No resources found

Every node should read Ready, and the VERSION column should match the Kubernetes version you configured above.

kubectl get pods -A prints every system pod, which is a lot to eyeball, so the second command inverts it: it shows only what has not reached Running or Completed. No resources found is the answer you want. Straight after provisioning give the CNI and the cloud-controller a minute to finish starting before treating anything listed there as a failure.

Next steps


Doc status

AspectDetail
Last reviewed5 September 2026