Skip to content

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
  • kubectl installed (see the Minikube Lab install steps if you skipped it)
  • Go is not required โ€” install the prebuilt binary below

1. Install kind

macOS

brew install kind

Linux

curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

Verify:

kind version
# kind v0.24.0 go1.22.6 linux/amd64

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:

kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
    image: kindest/node:v1.31.0
    extraPortMappings:
      - containerPort: 30080
        hostPort: 30080
        protocol: TCP
  - role: worker
    image: kindest/node:v1.31.0
  - role: worker
    image: kindest/node:v1.31.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

kind create cluster --name lab --config kind-config.yaml

Expected output (abbreviated):

Creating cluster "lab" ...
 โœ“ Ensuring node image (kindest/node:v1.31.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.31.0
# lab-worker           Ready    <none>          40s   v1.31.0
# lab-worker2          Ready    <none>          40s   v1.31.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:

Dockerfile
FROM nginx:1.27
RUN echo "served from a kind-loaded local image" > /usr/share/nginx/html/index.html
docker build -t local/hello-kind:1.0 .
docker images | grep local/hello-kind

5. Load the image into the cluster

kind load docker-image local/hello-kind:1.0 --name lab
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

hello-kind.yaml
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
kubectl apply -f hello-kind.yaml
kubectl rollout status deployment/hello-kind

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:

curl http://localhost:30080
# served from a kind-loaded local image

Troubleshooting

  • Pod stuck in ImagePullBackOff โ€” almost always imagePullPolicy wasn't set to IfNotPresent, or the image tag loaded doesn't exactly match the tag in the manifest.
  • curl localhost:30080 connection refused โ€” the extraPortMappings block only takes effect for nodes it's declared on; confirm it's under the control-plane node entry in kind-config.yaml and that you created the cluster with --config kind-config.yaml.
  • New image changes don't show up โ€” kind load docker-image doesn'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, check docker ps shows all three lab-* containers running and kubectl describe node <name> for the actual condition.

Cleanup

kubectl delete -f hello-kind.yaml
kind delete cluster --name lab
docker rmi local/hello-kind:1.0

Next

Continue to the Docker Desktop Lab to compare this against the Kubernetes cluster built into Docker Desktop.