Skip to content

Local cluster: K8s version

Setting the version

Use controlPlane.distro.k8s.image.tag:

controlPlane:
  distro:
    k8s:
      enabled: true
      image:
        tag: v1.35.0   # specify the Kubernetes version

image.registry/image.repository default to ghcr.io/loft-sh/kubernetes and don't need to be set unless pointing at a mirror. Omitting image.tag falls back to whatever vCluster's current default is — v1.36.0 as of this writing, but that default moves forward as new vCluster versions ship. Don't treat it as fixed; check the actual value on a live cluster with kubectl version rather than trusting this doc, and pin image.tag explicitly if you need reproducibility instead of relying on the default.

Confirmed working — tested both pinned and default

Verified directly against a live cluster by recreating with two different tags and checking kubectl get node -o wide:

Pinned to v1.35.0:

controlPlane:
  distro:
    k8s:
      enabled: true
      image:
        tag: v1.35.0
$ kubectl get node -o wide
NAME        STATUS   ROLES                  AGE    VERSION   CONTAINER-RUNTIME
local-k8s   Ready    control-plane,master   107s   v1.35.0   containerd://2.1.6
worker-1    Ready    <none>                 101s   v1.35.0   containerd://2.1.6
worker-2    Ready    <none>                 94s    v1.35.0   containerd://2.1.6
worker-3    Ready    <none>                 85s    v1.35.0   containerd://2.1.6

Default at time of testing (tag omitted, resolved to v1.36.0 — this will drift as vCluster ships new releases, don't assume it still applies):

$ kubectl get node -o wide
NAME        STATUS   ROLES                  AGE   VERSION   CONTAINER-RUNTIME
local-k8s   Ready    control-plane,master   28m   v1.36.0   containerd://2.2.3
worker-1    Ready    <none>                 28m   v1.36.0   containerd://2.2.3
worker-2    Ready    <none>                 28m   v1.36.0   containerd://2.2.3
worker-3    Ready    <none>                 28m   v1.36.0   containerd://2.2.3

VERSION matches the configured tag exactly on every node, control plane included — pinning genuinely applies cluster-wide, not just to the API server.

It's not just the Kubernetes version — the whole node image changes

Notice CONTAINER-RUNTIME differs too: containerd://2.1.6 on v1.35.0 vs containerd://2.2.3 on v1.36.0. Each image.tag is a full node image bundling a specific containerd build alongside that Kubernetes version — pinning the tag pins more than just kubectl version output, it pins the whole node runtime stack. Worth knowing if you're chasing a containerd-specific bug or behavior difference, not just a Kubernetes API version.

One more quirk observed during testing: the CLI log for a v1.35.0 pin shows Pulling image ghcr.io/loft-sh/kubernetes:v1.35.0-full — vind appends a -full suffix to whatever tag you specify internally; you don't need to (and shouldn't) write -full yourself in cluster.yaml.

Why pin at all

Without a pin, every developer's cluster resolves to whatever the current default tag is at the moment they run vcluster create — that default changes over time as vCluster releases new versions. Pinning keeps every developer's local-k8s cluster on the identical Kubernetes version, so "works on my machine" doesn't come down to who created their cluster more recently.

Caveat: requires full cluster recreation

Same as networking.podCIDR/serviceCIDR (see NETWORKING.md) — the node image is baked in at container-creation time, so changing the pinned version requires a full recreate, not --upgrade:

vcluster delete local-k8s
vcluster create local-k8s --values cluster.yaml