Kind in Docker Desktop: Multi‑Node Kubernetes, Instantly Available. A Perfect Companion for Kubestronaut Study!

DockerKubernetes
August 25, 2025
James Spurin
James Spurin
CNCF Ambassador • Kubestronaut • Docker Captain
Share this article:
Kind in Docker Desktop: Multi‑Node Kubernetes, Instantly Available. A Perfect Companion for Kubestronaut Study!

Image: One Click Multi-Node Kubeadm Kubernetes ... Hell Yeah!

Flip one switch in Docker Desktop and you'll have a real, multi‑node Kubernetes cluster built using kubeadm running in Docker containers - no VMs or Raspberry Pi clusters, no scripts, no waiting. The perfect infrastructure for learning on your Kubestronaut journey!

Why this matters:

  • Multi‑Node Kubernetes in Docker - start clusters in seconds, pin versions, and keep resource use low
  • Easily available - Kind is built into Docker Desktop; nothing extra to install
  • Study smarter - Ideal for KCNA, KCSA, CKA, CKAD practice; this can be used to cover the vast majority of exam scenarios (CKS is an exception, more details below)
  • Upstream release‑gated - Kind is the official test bed for Kubernetes releases. If it’s good enough to validate an official Kubernetes release, it’s good enough for your laptop!
  • Built upon kubeadm - This isn't a fork or an IoT distribution of Kubernetes. Kind uses the gold standard for Kubernetes installations - kubeadm

TL;DR - Docker Desktop’s native Kind provisioner makes local Kubernetes trivial, reproducible, and certification‑ready.


What is Kind?

Kind stands for Kubernetes IN Docker. It runs Kubernetes nodes as Docker containers with the control plane and workers running on your machine. No hypervisor/vm/cloud instance juggling, no hand‑rolled kubeadm scripts: a working cluster appears in seconds and behaves as Kubernetes should.

Out of the box it appears, as a convenient Kubernetes cluster that you can access locally, however, with some CLI magic we can access each and every node as if you'd built a cluster using kubeadm yourself!

Perfect for advanced scenarios and study, especially for those in the CKA/CKAD.

Design goals:

  • Docker container nodes (no per‑node VMs to manage)
  • Consistent, upstream‑aligned behavior (the project lives under Kubernetes SIGs)
  • Made for automation (great for CI, reproducible labs, and teaching)

Why Kind matters (it’s not another lightweight Kubernetes distro)

Kind isn’t a convenience fork of Kubernetes; it’s the distribution Kubernetes itself is tested on. Core contributors and SIGs rely on Kind for end‑to‑end testing and PR validation. That pedigree means:

  • Reliable behavior that closely tracks upstream versions
  • Reproducibility across laptops, CI runners, and teaching labs
  • Confidence that if it works in kind, it’s very likely to work on real clusters

If it’s good enough to gate a Kubernetes release, it’s good enough for everyone’s laptop.


What’s new: Native Kind in Docker Desktop

Docker Desktop recently added Kind as a built‑in cluster provisioner alongside the legacy single node kubeadm option. Benefits:

  • Zero manual install - no separate kind binary or scripts required
  • Multi‑node clusters - pick 1, 2, 3, 4, 5... nodes from the UI
  • Version selector - choose your Kubernetes version from a dropdown in the Docker Desktop UI; great for testing specific releases, upgrades and for reproducing bugs
  • Faster boot - typical clusters start in ~tens of seconds
  • Security alignment - integrates cleanly with Docker Desktop’s isolation features (Docker Desktop uses a hidden/isolated VM to run containers)

Enable it (1-2 minutes)

  1. Open Docker Desktop → Settings → Kubernetes
  2. Check Enable Kubernetes
  3. Under Cluster provisioning method, choose Kind
  4. Pick your Kubernetes version and number of nodes
  5. Click Apply

When it’s ready, your kubeconfig context will be docker-desktop and kubectl get nodes will work from your command prompt or terminal.

Version selection at cluster create time

When creating the cluster, use the Kubernetes version selector to pin a release (e.g. 1.33.x). This is invaluable for:

  • Exam prep: match the exam control plane minor version, test upgrades
  • Bug repro: roll back/forward to confirm version‑specific behaviour
  • Upgrade drills: recreate at the next minor and validate workloads

Why this is a big deal for day‑to‑day work

1) Streamlined for education, testing, and local dev

  • Workshops & self‑study: learners can build multi‑node labs with everyday laptops
  • Team testing: verify scheduling, anti‑affinity, PodDisruptionBudgets and upgrades-locally
  • Inner‑loop dev: iterate Helm charts, operators, and CI pipelines against the exact version you target in production

2) Minimal hardware requirements

  • Nodes are containers rather than VMs, so memory and CPU footprints are typically much smaller
  • Ideal for laptops and cloud dev environments (WSL2 etc.)

3) Version parity & repeatability

  • Pin a Kubernetes version per cluster to reproduce bugs or pre‑validate upgrades

Under the hood (and how to "look inside")

Kind clusters are built using kubeadm inside the node containers. That means you can explore the control plane like you would on a bare‑metal install-without maintaining VMs. We just need to be able to traverse from our local machine, to the internals of the cluster.

Let's check it out. With kind installed, open a terminal or command prompt. You'll see a Kubernetes cluster ready to use -

# Show available contexts
james@Mac ~ % kubectl config get-contexts
CURRENT   NAME             CLUSTER          AUTHINFO         NAMESPACE
*         docker-desktop   docker-desktop   docker-desktop

# List our nodes
james@Mac ~ % kubectl get nodes -o wide
NAME                    STATUS   ROLES           AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION     CONTAINER-RUNTIME
desktop-control-plane   Ready    control-plane   3m18s   v1.32.5   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   6.10.14-linuxkit   containerd://2.1.1
desktop-worker          Ready    <none>          3m9s    v1.32.5   172.18.0.3    <none>        Debian GNU/Linux 12 (bookworm)   6.10.14-linuxkit   containerd://2.1.1
desktop-worker2         Ready    <none>          3m9s    v1.32.5   172.18.0.4    <none>        Debian GNU/Linux 12 (bookworm)   6.10.14-linuxkit   containerd://2.1.1

# Check the current version
james@Mac ~ % kubectl version
Client Version: v1.32.2
Kustomize Version: v5.5.0
Server Version: v1.32.5

At this point, you have a fully functional cluster which is great for learning. However, we can take this further by navigating to the kubernetes containers direct. By default, the containers used via Kind in Docker Destkop will be hidden, you'll need to enable these by toggling the Show system containers option -

Show System Containers

  1. Open Docker Desktop → Settings → Kubernetes
  2. Check Show system containers (advanced)
  3. Click Apply

After doing so, we'll now be able to see our kind containers, in docker ps. Scroll to the right to see the container names of desktop-control-plane, desktop-worker and desktop-worker2.

james@Mac ~ % docker ps
CONTAINER ID   IMAGE                                                 COMMAND                  CREATED          STATUS          PORTS                       NAMES
283aacaa252a   docker/desktop-cloud-provider-kind:v0.3.0-desktop.3   "/bin/cloud-provider…"   10 minutes ago   Up 10 minutes   2375-2376/tcp               kind-cloud-provider
36f079951c02   docker/desktop-containerd-registry-mirror:v0.0.2      "/bin/registry"          10 minutes ago   Up 10 minutes                               kind-registry-mirror
a9041e01140e   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   10 minutes ago   Up 10 minutes                               desktop-worker
d4a4720f06a7   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   10 minutes ago   Up 10 minutes   127.0.0.1:60054->6443/tcp   desktop-control-plane
529fdbb8946b   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   10 minutes ago   Up 10 minutes                               desktop-worker2

Exec into a node container

# Docker exec into the Kubernetes control plane instance
james@Mac ~ % docker exec -it desktop-control-plane bash

# Check the version, this time using the short value
root@desktop-control-plane:/# kubeadm version -o short
v1.32.5

# Check the version of the kubelet
root@desktop-control-plane:/# kubelet --version
Kubernetes v1.32.5

# Use crictl ps to list containerd containers (kind uses containerd as its container runtime)
root@desktop-control-plane:/# crictl ps
CONTAINER           IMAGE               CREATED             STATE               NAME                      ATTEMPT             POD ID              POD                                             NAMESPACE
5b51c317e67ab       653e2348b2d4a       2 hours ago         Running             local-path-provisioner    0                   cf4c29517e03e       local-path-provisioner-7dc846544d-bb2br         local-path-storage
4a871609e5dc1       2f6c962e7b831       2 hours ago         Running             coredns                   0                   eb6641db5e8ae       coredns-668d6bf9bc-ffhk6                        kube-system
860987010859d       2f6c962e7b831       2 hours ago         Running             coredns                   0                   edbce365359f3       coredns-668d6bf9bc-f22j4                        kube-system
62a4696a31517       b1a8c6f707935       2 hours ago         Running             kindnet-cni               0                   65813d3224d03       kindnet-279nj                                   kube-system
73e39d461156c       69b7afc06f22e       2 hours ago         Running             kube-proxy                0                   aadd9f61bbd05       kube-proxy-84mv4                                kube-system
711517fec1763       7fc9d4aa817aa       2 hours ago         Running             etcd                      0                   4ebbebc01185d       etcd-desktop-control-plane                      kube-system
b0fbb87a97820       42968274c3d27       2 hours ago         Running             kube-apiserver            0                   53713994d399c       kube-apiserver-desktop-control-plane            kube-system
7120f86644eb6       82042044d6ea1       2 hours ago         Running             kube-controller-manager   0                   f07b3eb44ccb9       kube-controller-manager-desktop-control-plane   kube-system
49c1996b161fc       e149336437f90       2 hours ago         Running             kube-scheduler            0                   a1b08e56a44ff       kube-scheduler-desktop-control-plane            kube-system

# Demonstrate that we are in a kubeadm cluster, list /etc/kubernetes/manifests
root@desktop-control-plane:/# ls /etc/kubernetes/manifests/
etcd.yaml  kube-apiserver.yaml	kube-controller-manager.yaml  kube-scheduler.yaml

What you’ll notice:

  • /etc/kubernetes contains the usual manifests and certs
  • containerd runs pods; crictl lets you inspect them
  • You can safely explore without the overhead of a full VM per node

Certification & Training: where Kind shines

If you’re preparing for Kubernetes certifications, here’s the pragmatic guidance I give students:

  • KCNA (Associate): Perfect on Kind. Practice core objects, architecture, and ecosystem tooling locally
  • KCSA (Security Associate): Great fit - RBAC, network policies etc
  • CKA / CKAD: Excellent. You can replicate almost all admin/developer tasks, including upgrades, scheduling, debugging, and controllers
  • CKS: Mostly fine for cluster‑level security topics (RBAC, NetworkPolicy, image security, admission, etc.), but less ideal for kernel‑level tasks (e.g. distro‑specific hardening) that expect host access beyond container boundaries. Keep Kind for 80-90% of your practice and supplement with a lab VM when you need kernel modules or OS hardening (the shared Kernel in Docker Desktop isn't upgradable by default). Again, you'll be able to use kind but, with a native linux distribution

CKA‑style upgrade exercise: 1.32 → 1.33 (3 nodes)

Let's truly see this in action, we'll dive into a Kubernetes upgrade. Upgrading from what was at the time the previous version of Kubernetes (1.32) to the current version (1.33).

Why this matters: Upgrades are a classic CKA scenario. With Docker Desktop + Kind you can practice the kubeadm flow inside the node containers.

0) Start at v1.32 (3 nodes)

  • Docker Desktop → Settings → Kubernetes
  • Provisioner: Kind
  • Kubernetes version: 1.32.x, Nodes: 3
  • Click Apply

1) Verify current version (on your local system)

# Confirm that we're currently running 1.32.x
james@Mac ~ % kubectl get nodes
NAME                    STATUS   ROLES           AGE    VERSION
desktop-control-plane   Ready    control-plane   150m   v1.32.5
desktop-worker          Ready    <none>          150m   v1.32.5
desktop-worker2         Ready    <none>          150m   v1.32.5

2) Exec into the node (prepare kubeadm/kubelet/kubectl)

# Kind provides bundled binaries for kubeadm/kubelet/kubectl
root@desktop-control-plane:/# which kubeadm kubelet kubectl
/usr/bin/kubeadm
/usr/bin/kubelet
/usr/bin/kubectl

# We can see that these are not provided by an installed package
root@desktop-control-plane:/# dpkg -S /usr/bin/kubeadm /usr/bin/kubelet /usr/bin/kubectl
dpkg-query: no path found matching pattern /usr/bin/kubeadm
dpkg-query: no path found matching pattern /usr/bin/kubelet
dpkg-query: no path found matching pattern /usr/bin/kubectl

# We'll remove these, and later on will install newer versions, via a kubernetes package - Mimicking what we would have in a fully built kubeadm cluster
root@desktop-control-plane:/# rm /usr/bin/kubeadm /usr/bin/kubelet /usr/bin/kubectl

3) Upgrade the control plane

# Update apt for base packages
root@desktop-control-plane:/# apt-get update
Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
Get:4 http://deb.debian.org/debian bookworm/main arm64 Packages [8693 kB]
Get:5 http://deb.debian.org/debian bookworm-updates/main arm64 Packages [6936 B]
Get:6 http://deb.debian.org/debian-security bookworm-security/main arm64 Packages [270 kB]
Fetched 9225 kB in 1s (6696 kB/s)
Reading package lists... Done

# Install prerequisites
root@desktop-control-plane:/# apt-get install -y apt-transport-https ca-certificates curl gpg
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
curl is already the newest version (7.88.1-10+deb12u12).
The following additional packages will be installed:
  dirmngr gnupg gnupg-l10n gnupg-utils gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm libassuan0 libksba8 libnpth0 pinentry-curses
Suggested packages:
  dbus-user-session libpam-systemd pinentry-gnome3 tor parcimonie xloadimage scdaemon pinentry-doc
The following NEW packages will be installed:
  apt-transport-https dirmngr gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm libassuan0 libksba8 libnpth0 pinentry-curses
The following packages will be upgraded:
  ca-certificates
1 upgraded, 15 newly installed, 0 to remove and 8 not upgraded.
Need to get 7868 kB of archives.
<snip>

# Configure keyrings
root@desktop-control-plane:/# mkdir -p -m 755 /etc/apt/keyrings
root@desktop-control-plane:/# curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# Add Kubernetes repositories
root@desktop-control-plane:/# echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /

# Update repositories
root@desktop-control-plane:/# apt-get update
Hit:1 http://deb.debian.org/debian bookworm InRelease
Hit:2 http://deb.debian.org/debian bookworm-updates InRelease
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease
Get:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  InRelease [1186 B]
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  Packages [7611 B]
Fetched 8797 B in 1s (15.1 kB/s)
Reading package lists... Done

# Update available packages including our newly added Kubernetes repository
root@desktop-control-plane:/# apt-get update
Hit:1 http://deb.debian.org/debian bookworm InRelease
Hit:2 http://deb.debian.org/debian bookworm-updates InRelease
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease
Get:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  InRelease [1186 B]
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  Packages [7611 B]
Fetched 8797 B in 1s (15.1 kB/s)
Reading package lists... Done

# Install kubelet, kubeadm and kubectl as packages from the newly added repository
root@desktop-control-plane:/# apt-get install -y kubelet kubeadm kubectl
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  cri-tools kubernetes-cni
The following NEW packages will be installed:
  cri-tools kubeadm kubectl kubelet kubernetes-cni
0 upgraded, 5 newly installed, 0 to remove and 8 not upgraded.
Need to get 85.6 MB of archives.
After this operation, 339 MB of additional disk space will be used.
Get:1 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  cri-tools 1.33.0-1.1 [15.8 MB]
Get:2 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubeadm 1.33.4-1.1 [10.9 MB]
Get:3 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubectl 1.33.4-1.1 [9986 kB]
Get:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubernetes-cni 1.6.0-1.1 [35.3 MB]
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubelet 1.33.4-1.1 [13.5 MB]
<snip>
# Along the way you'll see this, Kind uses a custom configuration for the kubelet
# allowing the kubelet to run with swap enabled (required when running in a container where
# the parent system (i.e. Docker Desktop) has swap enabled)
# 
# We can continue with 'keep your currently-installed version' - ignore this message
Configuration file '/etc/default/kubelet'
 ==> File on system created by you or by a script.
 ==> File also in package provided by package maintainer.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainers version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** kubelet (Y/I/N/O/D/Z) [default=N] ?
<snip>

# Hold the new version
root@desktop-control-plane:/# apt-mark hold kubelet kubeadm kubectl
kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.

# Plan the upgrade
root@desktop-control-plane:/# kubeadm upgrade plan
[preflight] Running pre-flight checks.
[upgrade/config] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade/config] Use 'kubeadm init phase upload-config --config your-config-file' to re-upload it.
[upgrade] Running cluster health checks
[upgrade] Fetching available versions to upgrade to
[upgrade/versions] Cluster version: 1.32.5
[upgrade/versions] kubeadm version: v1.33.4
[upgrade/versions] Target version: v1.33.4
[upgrade/versions] Latest version in the v1.32 series: v1.32.8

Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT   NODE                    CURRENT   TARGET
kubelet     desktop-control-plane   v1.32.5   v1.32.8
kubelet     desktop-worker          v1.32.5   v1.32.8
kubelet     desktop-worker2         v1.32.5   v1.32.8

Upgrade to the latest version in the v1.32 series:

COMPONENT                 NODE                    CURRENT    TARGET
kube-apiserver            desktop-control-plane   v1.32.5    v1.32.8
kube-controller-manager   desktop-control-plane   v1.32.5    v1.32.8
kube-scheduler            desktop-control-plane   v1.32.5    v1.32.8
kube-proxy                                        1.32.5     v1.32.8
CoreDNS                                           v1.11.3    v1.12.0
etcd                      desktop-control-plane   3.5.16-0   3.5.21-0

You can now apply the upgrade by executing the following command:

	kubeadm upgrade apply v1.32.8

_____________________________________________________________________

Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT   NODE                    CURRENT   TARGET
kubelet     desktop-control-plane   v1.32.5   v1.33.4
kubelet     desktop-worker          v1.32.5   v1.33.4
kubelet     desktop-worker2         v1.32.5   v1.33.4

Upgrade to the latest stable version:

COMPONENT                 NODE                    CURRENT    TARGET
kube-apiserver            desktop-control-plane   v1.32.5    v1.33.4
kube-controller-manager   desktop-control-plane   v1.32.5    v1.33.4
kube-scheduler            desktop-control-plane   v1.32.5    v1.33.4
kube-proxy                                        1.32.5     v1.33.4
CoreDNS                                           v1.11.3    v1.12.0
etcd                      desktop-control-plane   3.5.16-0   3.5.21-0

You can now apply the upgrade by executing the following command:

	kubeadm upgrade apply v1.33.4

_____________________________________________________________________


The table below shows the current state of component configs as understood by this version of kubeadm.
Configs that have a "yes" mark in the "MANUAL UPGRADE REQUIRED" column require manual config upgrade or
resetting to kubeadm defaults before a successful upgrade can be performed. The version to manually
upgrade to is denoted in the "PREFERRED VERSION" column.

API GROUP                 CURRENT VERSION   PREFERRED VERSION   MANUAL UPGRADE REQUIRED
kubeproxy.config.k8s.io   v1alpha1          v1alpha1            no
kubelet.config.k8s.io     v1beta1           v1beta1             no
_____________________________________________________________________

# Apply the update
root@desktop-control-plane:/# kubeadm upgrade apply v1.33.4
[upgrade] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade] Use 'kubeadm init phase upload-config --config your-config-file' to re-upload it.
[upgrade/preflight] Running preflight checks
[upgrade] Running cluster health checks
[upgrade/preflight] You have chosen to upgrade the cluster version to "v1.33.4"
[upgrade/versions] Cluster version: v1.32.5
[upgrade/versions] kubeadm version: v1.33.4
[upgrade] Are you sure you want to proceed? [y/N]: y
[upgrade/preflight] Pulling images required for setting up a Kubernetes cluster
[upgrade/preflight] This might take a minute or two, depending on the speed of your internet connection
[upgrade/preflight] You can also perform this action beforehand using 'kubeadm config images pull'
[upgrade/control-plane] Upgrading your static Pod-hosted control plane to version "v1.33.4" (timeout: 5m0s)...
[upgrade/staticpods] Writing new Static Pod manifests to "/etc/kubernetes/tmp/kubeadm-upgraded-manifests2488743584"
[upgrade/staticpods] Preparing for "etcd" upgrade
[upgrade/staticpods] Renewing etcd-server certificate
[upgrade/staticpods] Renewing etcd-peer certificate
[upgrade/staticpods] Renewing etcd-healthcheck-client certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/etcd.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2025-08-22-18-01-04/etcd.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 1 Pods for label selector component=etcd
[upgrade/staticpods] Component "etcd" upgraded successfully!
[upgrade/etcd] Waiting for etcd to become available
[upgrade/staticpods] Preparing for "kube-apiserver" upgrade
[upgrade/staticpods] Renewing apiserver certificate
[upgrade/staticpods] Renewing apiserver-kubelet-client certificate
[upgrade/staticpods] Renewing front-proxy-client certificate
[upgrade/staticpods] Renewing apiserver-etcd-client certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-apiserver.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2025-08-22-18-01-04/kube-apiserver.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 1 Pods for label selector component=kube-apiserver
[upgrade/staticpods] Component "kube-apiserver" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-controller-manager" upgrade
[upgrade/staticpods] Renewing controller-manager.conf certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-controller-manager.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2025-08-22-18-01-04/kube-controller-manager.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 1 Pods for label selector component=kube-controller-manager
[upgrade/staticpods] Component "kube-controller-manager" upgraded successfully!
[upgrade/staticpods] Preparing for "kube-scheduler" upgrade
[upgrade/staticpods] Renewing scheduler.conf certificate
[upgrade/staticpods] Moving new manifest to "/etc/kubernetes/manifests/kube-scheduler.yaml" and backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-2025-08-22-18-01-04/kube-scheduler.yaml"
[upgrade/staticpods] Waiting for the kubelet to restart the component
[upgrade/staticpods] This can take up to 5m0s
[apiclient] Found 1 Pods for label selector component=kube-scheduler
[upgrade/staticpods] Component "kube-scheduler" upgraded successfully!
[upgrade/control-plane] The control plane instance for this node was successfully upgraded!
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config" in namespace kube-system with the configuration for the kubelets in the cluster
[upgrade/kubeconfig] The kubeconfig files for this node were successfully upgraded!
W0822 18:03:56.953751   10262 postupgrade.go:117] Using temporary directory /etc/kubernetes/tmp/kubeadm-kubelet-config1216020020 for kubelet config. To override it set the environment variable KUBEADM_UPGRADE_DRYRUN_DIR
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config1216020020/config.yaml
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/bootstrap-token] Configuring bootstrap token and cluster-info RBAC rules
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] Configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] Configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] Configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

[upgrade] SUCCESS! A control plane node of your cluster was upgraded to "v1.33.4".

[upgrade] Now please proceed with upgrading the rest of the nodes by following the right order.

# Check the nodes, currently the control-plane is updated
root@desktop-control-plane:/# kubectl get nodes
NAME                    STATUS   ROLES           AGE    VERSION
desktop-control-plane   Ready    control-plane   179m   v1.33.4
desktop-worker          Ready    <none>          179m   v1.32.5
desktop-worker2         Ready    <none>          179m   v1.32.5

We can now proceed with updating the nodes, repeating a similar process on desktop-worker and desktop-worker2. We'll hop on to the worker nodes from the local system.

# Exit from the control-plane back to our local system
root@desktop-control-plane:/# exit
exit

# Show the docker containers running the kubernetes nodes
james@Mac ~ % docker ps
CONTAINER ID   IMAGE                                                 COMMAND                  CREATED       STATUS       PORTS                       NAMES
283aacaa252a   docker/desktop-cloud-provider-kind:v0.3.0-desktop.3   "/bin/cloud-provider…"   3 hours ago   Up 3 hours   2375-2376/tcp               kind-cloud-provider
36f079951c02   docker/desktop-containerd-registry-mirror:v0.0.2      "/bin/registry"          3 hours ago   Up 3 hours                               kind-registry-mirror
a9041e01140e   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   3 hours ago   Up 3 hours                               desktop-worker
d4a4720f06a7   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   3 hours ago   Up 3 hours   127.0.0.1:60054->6443/tcp   desktop-control-plane
529fdbb8946b   kindest/node:v1.32.5                                  "/usr/local/bin/entr…"   3 hours ago   Up 3 hours                               desktop-worker2

# Access the first worker node
james@Mac ~ % docker exec -it desktop-worker bash

# Remove the kind bundled kubeadm, kubelet and kubectl
root@desktop-worker:/# rm /usr/bin/kubeadm /usr/bin/kubelet /usr/bin/kubectl

# Update apt for base packages
root@desktop-worker:/# apt-get update
Get:1 http://deb.debian.org/debian bookworm InRelease [151 kB]
Get:2 http://deb.debian.org/debian bookworm-updates InRelease [55.4 kB]
Get:3 http://deb.debian.org/debian-security bookworm-security InRelease [48.0 kB]
Get:4 http://deb.debian.org/debian bookworm/main arm64 Packages [8693 kB]
Get:5 http://deb.debian.org/debian bookworm-updates/main arm64 Packages [6936 B]
Get:6 http://deb.debian.org/debian-security bookworm-security/main arm64 Packages [270 kB]
Fetched 9225 kB in 1s (6644 kB/s)
Reading package lists... Done

# Install prerequisites
root@desktop-worker:/# apt-get install -y apt-transport-https ca-certificates curl gpg
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
curl is already the newest version (7.88.1-10+deb12u12).
The following additional packages will be installed:
  dirmngr gnupg gnupg-l10n gnupg-utils gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm libassuan0 libksba8 libnpth0 pinentry-curses
Suggested packages:
  dbus-user-session libpam-systemd pinentry-gnome3 tor parcimonie xloadimage scdaemon pinentry-doc
The following NEW packages will be installed:
  apt-transport-https dirmngr gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm libassuan0 libksba8 libnpth0 pinentry-curses
The following packages will be upgraded:
  ca-certificates
1 upgraded, 15 newly installed, 0 to remove and 8 not upgraded.
Need to get 7868 kB of archives.
After this operation, 16.8 MB of additional disk space will be used.
Get:1 http://deb.debian.org/debian bookworm-updates/main arm64 ca-certificates all 20230311+deb12u1 [155 kB]
Get:2 http://deb.debian.org/debian bookworm/main arm64 apt-transport-https all 2.6.1 [25.2 kB]
Get:3 http://deb.debian.org/debian bookworm/main arm64 libassuan0 arm64 2.5.5-5 [45.9 kB]
Get:4 http://deb.debian.org/debian bookworm/main arm64 gpgconf arm64 2.2.40-1.1 [557 kB]
Get:5 http://deb.debian.org/debian bookworm/main arm64 libksba8 arm64 1.6.3-2 [119 kB]
Get:6 http://deb.debian.org/debian bookworm/main arm64 libnpth0 arm64 1.6-3 [18.6 kB]
<snip>

# Configure keyrings
root@desktop-worker:/# mkdir -p -m 755 /etc/apt/keyrings
root@desktop-worker:/# curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.33/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg

# Add Kubernetes repositories
root@desktop-worker:/# echo 'deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /' | tee /etc/apt/sources.list.d/kubernetes.list
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.33/deb/ /

# Update repositories
root@desktop-worker:/# apt-get update
Hit:1 http://deb.debian.org/debian bookworm InRelease
Hit:2 http://deb.debian.org/debian bookworm-updates InRelease
Hit:3 http://deb.debian.org/debian-security bookworm-security InRelease
Get:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  InRelease [1186 B]
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  Packages [7611 B]
Fetched 8797 B in 1s (14.4 kB/s)
Reading package lists... Done

# Install kubelet, kubeadm and kubectl
root@desktop-worker:/# apt-get install -y kubelet kubeadm kubectl
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following additional packages will be installed:
  cri-tools kubernetes-cni
The following NEW packages will be installed:
  cri-tools kubeadm kubectl kubelet kubernetes-cni
0 upgraded, 5 newly installed, 0 to remove and 8 not upgraded.
Need to get 85.6 MB of archives.
After this operation, 339 MB of additional disk space will be used.
Get:1 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  cri-tools 1.33.0-1.1 [15.8 MB]
Get:2 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubeadm 1.33.4-1.1 [10.9 MB]
Get:3 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubectl 1.33.4-1.1 [9986 kB]
Get:4 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubernetes-cni 1.6.0-1.1 [35.3 MB]
Get:5 https://prod-cdn.packages.k8s.io/repositories/isv:/kubernetes:/core:/stable:/v1.33/deb  kubelet 1.33.4-1.1 [13.5 MB]
Fetched 85.6 MB in 2s (37.1 MB/s)
debconf: delaying package configuration, since apt-utils is not installed
Selecting previously unselected package cri-tools.
(Reading database ... 9884 files and directories currently installed.)
Preparing to unpack .../cri-tools_1.33.0-1.1_arm64.deb ...
Unpacking cri-tools (1.33.0-1.1) ...
Selecting previously unselected package kubeadm.
Preparing to unpack .../kubeadm_1.33.4-1.1_arm64.deb ...
Unpacking kubeadm (1.33.4-1.1) ...
Selecting previously unselected package kubectl.
Preparing to unpack .../kubectl_1.33.4-1.1_arm64.deb ...
Unpacking kubectl (1.33.4-1.1) ...
Selecting previously unselected package kubernetes-cni.
Preparing to unpack .../kubernetes-cni_1.6.0-1.1_arm64.deb ...
Unpacking kubernetes-cni (1.6.0-1.1) ...
Selecting previously unselected package kubelet.
Preparing to unpack .../kubelet_1.33.4-1.1_arm64.deb ...
Unpacking kubelet (1.33.4-1.1) ...
Setting up kubectl (1.33.4-1.1) ...
Setting up cri-tools (1.33.0-1.1) ...
Setting up kubernetes-cni (1.6.0-1.1) ...
Setting up kubeadm (1.33.4-1.1) ...
Setting up kubelet (1.33.4-1.1) ...

Configuration file '/etc/default/kubelet'
 ==> File on system created by you or by a script.
 ==> File also in package provided by package maintainer.
   What would you like to do about it ?  Your options are:
    Y or I  : install the package maintainers version
    N or O  : keep your currently-installed version
      D     : show the differences between the versions
      Z     : start a shell to examine the situation
 The default action is to keep your current version.
*** kubelet (Y/I/N/O/D/Z) [default=N] ?

# Hold the new version
root@desktop-worker:/# apt-mark hold kubelet kubeadm kubectl
kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.

# Upgrade node
root@desktop-worker:/# kubeadm upgrade node
[upgrade] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...
[upgrade] Use 'kubeadm init phase upload-config --config your-config-file' to re-upload it.
[upgrade/preflight] Running pre-flight checks
[upgrade/preflight] Skipping prepull. Not a control plane node.
[upgrade/control-plane] Skipping phase. Not a control plane node.
[upgrade/kubeconfig] Skipping phase. Not a control plane node.
W0822 18:14:56.518328    8123 postupgrade.go:117] Using temporary directory /etc/kubernetes/tmp/kubeadm-kubelet-config133089756 for kubelet config. To override it set the environment variable KUBEADM_UPGRADE_DRYRUN_DIR
[upgrade] Backing up kubelet config file to /etc/kubernetes/tmp/kubeadm-kubelet-config133089756/config.yaml
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/addon] Skipping the addon/coredns phase. Not a control plane node.
[upgrade/addon] Skipping the addon/kube-proxy phase. Not a control plane node.

# Restart the kubelet
root@desktop-worker:/# systemctl restart kubelet

Repeat the process for the remaining node desktop-worker2 and then, afterwards we can confirm with kubectl get nodes, that the cluster was updated!

james@Mac ~ % docker exec -it desktop-control-plane bash
root@desktop-control-plane:/# kubectl get nodes
NAME                    STATUS   ROLES           AGE     VERSION
desktop-control-plane   Ready    control-plane   3h10m   v1.33.4
desktop-worker          Ready    <none>          3h10m   v1.33.4
desktop-worker2         Ready    <none>          3h10m   v1.33.4

Gotchas & Notes

  • Containerd image store: Kind uses containerd
  • Context conflicts: If kubectl points to another environment (e.g. minikube), switch to docker-desktop
  • System containers hidden by default: toggle Show system containers to inspect them with docker ps

Educator’s Take & Call To Action

For learners and teams, Kind in Docker Desktop is a great option:

  • It removes the friction from "getting a real cluster"
  • We don't need to overcomplicate this for learning purposes
  • It mirrors upstream behavior and versions
  • Kind is used to test and validate Kubernetes
  • It scales from a single laptop to a CI fleet

Get Started


If this helped, share it with your team. The less time we spend wrestling local clusters, the more time we spend learning, building, and contributing.

Share this article:
James Spurin

James Spurin

CNCF Ambassador • Kubestronaut • Docker Captain

James Spurin is a distinguished expert in cloud-native technologies, backed by over 25 years of professional industry experience spanning infrastructure engineering, DevOps, and modern container orchestration. As a CNCF Ambassador and Docker Captain, he regularly contributes to the community through speaking engagements, workshops, and technical content.