Skip to content

Migration — moving in and out

No lock-in is only a promise if you can actually exercise it. This page documents handover (moving a cluster between kubehz’s hosted control plane and your own infrastructure) in both directions, exactly as it works today.

What’s live today

Hosted → self-hosted (eject) is live. You can move your hosted control plane onto your own Hetzner infrastructure and keep the cluster’s cryptographic identity: your workers, kubeconfigs, and secrets keep working.

Self-hosted → hosted (adopt) is live as recreation. kubehz creates a fresh hosted control plane and you move your workloads onto it. The cluster gets a new identity. An identity-preserving adopt is planned; until it ships, this page says so plainly rather than pretending otherwise.

For the concepts behind all of this (why identity is the thing that moves, and why nothing is deleted without your say-so), see How handover works.

The two directions

DirectionWhat happensStatus
Eject: hosted → your infraYour hosted control plane is restored onto machines you own, with the same identity. Workers keep working.Live
Adopt: your cluster → hostedkubehz creates a fresh hosted control plane; you move workloads onto it. New identity.Live (recreation)

Eject is for when you want to take full ownership: compliance needs, cost control at scale, or simply because you can. The exit door is the proof that hosted is a choice, not a trap.

Adopt is for when you are done operating a control plane yourself and want kubehz to run it, keeping only workers on your account.

Assessment comes first

Nothing moves until you have seen the plan. The platform continuously collects a read-only assessment of your cluster (Kubernetes version, datastore, storage classes, load balancers, CAPI management) and derives a feasibility path plus a translation report, the list of things that will not translate automatically and need your attention:

  • Provider-coupled storage classes: volumes provisioned by one provider’s CSI driver do not follow the control plane to another.
  • LoadBalancer Services: cloud load balancers are bound to the provider that created them.
  • CAPI-managed clusters: a cluster whose lifecycle is driven by Cluster API needs its management paused before a handover makes sense.

See it any time from the CLI (it reads your tenant’s registry, so it needs KUBEHZ_TOKEN):

bash
lo kubehz assess

The dashboard shows the same assessment and feasibility on the cluster page. The feasibility path is one of:

PathMeaning
restoreMove via snapshot + identity seed. What eject uses today.
recreationA fresh control plane; workloads move by re-applying. What adopt uses today.
graftA live, in-place transfer. Assessed and reported, but not yet executable; it is on the roadmap.

Hosted → self-hosted (eject)

Eject moves your hosted control plane onto infrastructure you own, keeping the cluster’s identity: the certificate authority, the service-account signing keys, the secrets-encryption key, and every object in the cluster (via an etcd snapshot). Because the identity is preserved:

  • Your workers keep working: they already trust the cluster’s CA, and after cutover they reconnect to the same logical cluster.
  • Existing kubeconfigs and service-account tokens stay valid.
  • Secrets decrypt: the encryption key travels with the cluster.

The flow

  1. Assess: lo kubehz assess (or the dashboard). Confirm the path is restore and read the translation report.

  2. Start the handover: from the cluster’s Handover tab in the dashboard (or the API). You choose the target driver (kubeadm or kubeone) and whether to keep or rotate the endpoint.

  3. The platform exports: it assembles the export bundle (the cluster’s PKI, service-account keys, and encryption key) and takes a fresh etcd snapshot.

  4. Download the bundle: downloading requires re-authentication, is one-time, and is audit-logged. The bundle is your cluster’s root identity: treat it like a root credential and delete your local copy once the handover completes.

  5. Prepare the target: a machine (or machines) you own, reachable by your workers. Concretely:

    • Reachable on 6443 from every worker: that is the API endpoint lok8s configures. A target behind NAT with no inbound 6443 is the usual cause of a restore that appears to succeed and then strands the workers.
    • Sized above the reservations, not at them. On a control-plane node lok8s reserves 400m CPU and 1 GiB of memory for the system and kubelet before any workload, and the kubelet evicts once available memory drops under 500 MiB. A 2 GB machine is therefore the floor, not a working size. Our own reference configs use cx33 for control planes. See the KubeOne guide.
    • One or three nodes, never two: etcd needs an odd number to hold quorum.
    • A node runs at most 110 pods, which is what the control-plane kubelet is configured for.
    • lok8s installed on the target, and privileges to use it. receive runs on that machine, not from your workstation: it writes /etc/kubernetes and /var/lib/etcd and calls kubeadm.
    • The bundle on the target. Step 4 downloads it wherever you ran it; move it across a channel you trust and delete both copies once step 7 confirms. It is your cluster’s root identity in transit.
    • A clean node. receive refuses a machine that already carries Kubernetes state; on a previously-used node run kubeadm reset yourself first.
  6. Restore on the target:

    bash
    # kubeadm driver: run on the target node itself
    lo kubehz handover receive --bundle ./bundle.tar.gz
    
    # kubeone driver: pre-seed the identity, then provision as usual
    lo kubehz handover preseed --bundle ./bundle.tar.gz --node "<node-ip>"
    lo provision

    receive takes --snapshot <file> to restore an etcd snapshot you downloaded separately, --single-node for a one-node control plane, and --force to overwrite a node that already carries Kubernetes state (instead of the kubeadm reset above). preseed reaches the node over SSH: --user (default root), --port (default 22) and --ssh-key select how.

    receive seeds the exported identity, restores the etcd snapshot, and brings up a control plane that is your cluster: same CA, same keys, same objects.

  7. Cut over: point the cluster’s endpoint DNS at your new control plane. Workers reconnect on their own; nothing on them changes. Then confirm the cutover: the platform waits for your explicit confirmation and does nothing destructive before it.

  8. Decommission: the old hosted control plane is removed only after two things are true. The platform must have seen your new control plane alive (heartbeating), and you must have explicitly confirmed decommissioning. Until both, the old control plane stays intact as your fallback.

What you do at cutover

The one action that is yours alone: repoint the cluster’s endpoint DNS at the new control plane. Everything before it is reversible; the platform never flips your DNS for you. If anything looks wrong, don’t confirm: a halted handover keeps all state on both sides.

Self-hosted → hosted (adopt)

Adopt hands the control plane’s operation to kubehz: you keep only workers on your Hetzner account. Today, adopt works by recreation, and we say that plainly:

  • kubehz creates a fresh hosted control plane with a new identity (new CA, new keys). It is a new cluster on the platform.
  • Your workloads move by re-applying them (from your GitOps repo or manifests) onto the new cluster. This is the moment a declarative setup pays off.
  • Persistent data does not move automatically. Restore it from your own backups, or re-provision it. There is no automated data mover yet, so we will not pretend there is one.
  • Nodes join fresh: workers come up against the new control plane via worker pools; old kubeconfigs and tokens do not carry over.

The assessment and translation report run first, exactly as for eject, so you know about provider-coupled storage, load balancers, and CAPI management before anything is created.

Why recreation? The managed control-plane stack mints a cluster’s identity at creation time. Seeding your existing identity into it reliably requires a create-paused-then-seed step that is designed and planned. When it ships, adopt will preserve identity the way eject already does. Until then, adopt means: fresh control plane, workloads re-applied.

No lock-in, with or without migrating

Handover is the strong form of the promise, but you were never locked in to begin with:

  • lok8s is open source and provisions your cluster on your Hetzner account.
  • The dashboard integration is opt-in and outbound-only; kubehz holds no inbound credentials to your cluster.
  • Removing kubehz from a self-hosted cluster is two commands: lo kubehz deregister unregisters it (it needs KUBEHZ_TOKEN), and deleting the kubehz-system namespace removes the in-cluster agent. The agent’s cluster-scoped RBAC (its ClusterRole and ClusterRoleBinding, plus a Role in kube-system) survives the namespace; delete those objects too if you want a clean cluster. Your cluster keeps running exactly as before.

Next steps


Doc status

AspectDetail
StateEject live (restore-based); adopt live as recreation; identity-preserving adopt planned
Last reviewed5 September 2026