kind Lab¶
kind (Kubernetes IN Docker) runs each cluster node as a Docker container. That makes it fast to create, trivial to script, and — uniquely useful for local development — able to load an image straight from your machine's Docker daemon into the cluster without ever pushing it to a registry.
This lab creates a three-node cluster from a config file, builds a small local image, loads it into the cluster, and deploys it.
Prerequisites¶
- Docker installed and running
kubectlinstalled (see the Minikube Lab install steps if you skipped it)- Go is not required — install the prebuilt binary below
1. Install kind¶
macOS
Linux
# Take the current version from https://github.com/kubernetes-sigs/kind/releases
KIND_VERSION=v0.30.0
ARCH=$(uname -m | sed 's/x86_64/amd64/; s/aarch64/arm64/')
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-${ARCH}"
sudo install -m 0755 ./kind /usr/local/bin/kind
Verify:
2. Define a multi-node cluster¶
Create a config file describing one control-plane node and two workers, and pin the node image so every run gives you the same Kubernetes version. Always take the tag (ideally with its @sha256 digest) from the release notes of the kind version you installed — node images are built for specific kind releases:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
image: kindest/node:v1.37.0
extraPortMappings:
- containerPort: 30080
hostPort: 30080
protocol: TCP
- role: worker
image: kindest/node:v1.37.0
- role: worker
image: kindest/node:v1.37.0
extraPortMappings on the control-plane node is what lets a NodePort service inside the cluster be reachable at localhost:30080 on your machine later in this lab.
3. Create the cluster¶
Expected output (abbreviated):
Creating cluster "lab" ...
✓ Ensuring node image (kindest/node:v1.37.0) 🖼
✓ Preparing nodes 📦 📦 📦
✓ Writing configuration 📜
✓ Starting control-plane 🕹️
✓ Installing CNI 🔌
✓ Installing StorageClass 💾
✓ Joining worker nodes 🚜
Set kubectl context to "kind-lab"
You can now use your cluster with:
kubectl cluster-info --context kind-lab
kind create cluster already switched your current kubectl context to kind-lab. Confirm all three nodes are Ready:
kubectl config current-context
# kind-lab
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# lab-control-plane Ready control-plane 60s v1.37.0
# lab-worker Ready <none> 40s v1.37.0
# lab-worker2 Ready <none> 40s v1.37.0
4. Build a local image¶
Create a tiny app so the point of this lab — loading an image you built locally, with no registry involved — is obvious:
FROM nginx:1.27
RUN echo "served from a kind-loaded local image" > /usr/share/nginx/html/index.html
5. Load the image into the cluster¶
Image: "local/hello-kind:1.0" with ID "sha256:..." not yet present on node "lab-control-plane", loading...
Image: "local/hello-kind:1.0" with ID "sha256:..." not yet present on node "lab-worker", loading...
Image: "local/hello-kind:1.0" with ID "sha256:..." not yet present on node "lab-worker2", loading...
kind load docker-image copies the image directly into containerd on every node in the named cluster. No registry, no docker push, no image pull secret — this is the fastest inner loop for testing a locally built image against real Kubernetes scheduling and networking.
Set imagePullPolicy: IfNotPresent
If the Deployment's imagePullPolicy is Always (or unset on a tag Kubernetes treats as mutable), the kubelet will try to pull local/hello-kind:1.0 from a registry, fail, and the pod will sit in ImagePullBackOff — even though the image is already loaded onto the node. Explicitly setting IfNotPresent is what makes the loaded image actually get used.
6. Deploy and expose it¶
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-kind
labels:
app: hello-kind
spec:
replicas: 3
selector:
matchLabels:
app: hello-kind
template:
metadata:
labels:
app: hello-kind
spec:
containers:
- name: hello-kind
image: local/hello-kind:1.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: hello-kind
spec:
type: NodePort
selector:
app: hello-kind
ports:
- port: 80
targetPort: 80
nodePort: 30080
7. Verify¶
Confirm the pods actually landed on different worker nodes — this is the multi-node scheduling behavior a single-node minikube cluster can't demonstrate:
kubectl get pods -o wide
# NAME READY STATUS NODE
# hello-kind-6c8f9d5b7f-2f4kk 1/1 Running lab-worker
# hello-kind-6c8f9d5b7f-9h2lm 1/1 Running lab-worker2
# hello-kind-6c8f9d5b7f-vx8qc 1/1 Running lab-worker
Because the control-plane node maps container port 30080 to localhost:30080 on your machine (from the extraPortMappings in step 2), you can curl it directly — no minikube service / kubectl port-forward needed:
Troubleshooting¶
- Pod stuck in
ImagePullBackOff— almost alwaysimagePullPolicywasn't set toIfNotPresent, or the image tag loaded doesn't exactly match the tag in the manifest. curl localhost:30080connection refused — theextraPortMappingsblock only takes effect for nodes it's declared on; confirm it's under thecontrol-planenode entry inkind-config.yamland that you created the cluster with--config kind-config.yaml.kind load docker-imagefails with a digest or "not found" error on Docker Desktop — Docker Desktop's newer containerd image store can export images in a waykind load docker-imagedoesn't handle, especially multi-platform images. Load through an archive instead:docker save local/hello-kind:1.0 -o hello.tar && kind load image-archive hello.tar --name lab.- New image changes don't show up —
kind load docker-imagedoesn't overwrite an already-loaded image with the same tag reliably; bump the tag (:1.1) or delete the pods to force a re-pull from containerd's local cache. - Nodes stuck
NotReady— kind installs its own CNI automatically; if this happens, checkdocker psshows all threelab-*containers running andkubectl describe node <name>for the actual condition.
Cleanup¶
Next¶
Continue to the Docker Desktop Lab to compare this against the Kubernetes cluster built into Docker Desktop.