Cloud sandbox expired and the tutorial still says “apply this YAML.”

You need a sharper question answered first: can I stand up a real Kubernetes API on my laptop, break things safely, and delete the whole lab in one command?

That is what kind (Kubernetes IN Docker) is for. Nodes are containers. The control plane is local. You get a disposable cluster for training, chart experiments, and POCs without a cloud bill or a shared staging fight. The sections below walk from install through a smoke deploy and a clean teardown — so each step feels intentional, not magical.

Why kind for learning and POCs

kind is built for ephemeral clusters. Create one, try an idea, delete it. Multi-node layouts are a config file away. CI systems already use it for the same reason: cheap, repeatable, Docker-backed.

Quick contrast when someone asks “why not X?”:

ToolBest fitTrade-off
kindLocal labs, CI, multi-node experimentsNeeds Docker (or a compatible runtime); networking is container-on-container
minikubeClassic laptop cluster, many driversHeavier footprint; more “product” surface than you need for a quick POC
k3dFast k3s-in-Docker labsk3s packaging differs from upstream kubeadm-style clusters

Pick kind when you want upstream-looking Kubernetes and a cluster you are willing to throw away. Pick something else when your team already standardized on it — the kubectl muscle stays the same.

Note: kind is not a stand-in for production networking, ingress SLAs, or cloud IAM. Use it to learn objects and workflows; prove prod assumptions in an environment that matches prod.

When you need a production-grade cluster

kind teaches the Kubernetes API on your laptop. Production usually means a managed control plane or a deliberate bootstrap/lifecycle tool — not Docker-backed nodes on a single workstation.

PathExamplesWhen teams pick it
Managed cloud KubernetesAmazon EKS, Google GKE, Azure AKSShared responsibility, cloud IAM/networking, least ops toil for most product teams
Self-managed / bootstrapkubeadm, kops, Cluster APIFull control of control-plane and node lifecycle on your own (or hybrid) infrastructure
Platform distributionsOpenShift, Rancher-managed clustersOpinionated platform layer on top of Kubernetes when the enterprise wants that packaging

Note: This post stays on kind for learning and POCs. Reach for EKS/GKE/AKS (or the bootstrap and platform paths above) when you need prod SLAs, multi-AZ, and cloud identity — the kubectl habits transfer; the networking and IAM story does not.

Prerequisites

You need three pieces before kind create cluster succeeds:

  1. A container runtime — Docker Desktop, Docker Engine, or a compatible runtime kind supports
  2. kubectl — talks to the API server kind wires into your kubeconfig
  3. kind — the CLI that creates and deletes clusters

Confirm Docker answers:

docker info >/dev/null && echo "docker ok"

Typical healthy signal:

docker ok

Install kubectl if you do not have it yet (pick the path your OS docs recommend), then confirm:

kubectl version --client

Install kind. On Linux, a common pattern is a released binary. Pin a version from the kind releases page so everyone on the team matches:

# Example pin — bump when you intentionally upgrade
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64

Make the download executable:

chmod +x ./kind

Move it onto your PATH (needs admin rights on most Linux installs):

sudo mv ./kind /usr/local/bin/kind

macOS users often prefer Homebrew (brew install kind). Either way, confirm the CLI answers:

kind version

Note: If Docker is not running, kind fails early with a daemon error — fix the runtime first, not kubeconfig.

Create a single-node cluster

Name the lab so it does not collide with a default kind context on a shared machine:

kind create cluster --name geekmonks-lab

kind pulls a node image (first run is slower), starts a control-plane container, and writes a kubeconfig context. Walk three checks so you know kubectl is talking to this lab — not a leftover cloud cluster.

Print the active context name. After a successful create it should look like kind-<cluster-name>:

kubectl config current-context
kind-geekmonks-lab

Ask whether the API server behind that context is reachable (endpoint URLs and “is running” lines):

kubectl cluster-info

Typical shape (host/port differ):

Kubernetes control plane is running at https://127.0.0.1:xxxxx
CoreDNS is running at https://127.0.0.1:xxxxx/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

List nodes so you see the control-plane container Kubernetes thinks is a machine:

kubectl get nodes
NAME                          STATUS   ROLES           AGE   VERSION
geekmonks-lab-control-plane   Ready    control-plane   40s   v1.32.x

STATUS Ready on the control-plane node means the API is usable. If the context is wrong, switch explicitly:

kubectl config use-context kind-geekmonks-lab

Note: Blind kubectl get pods against a leftover cloud context is how “nothing is deployed” becomes a false alarm. Confirm context before every lab session.

Optional: multi-node for realistic training

A single node is enough for most YAML drills. When you want scheduling across workers — affinity demos, “why is this pod Pending?” labs — give kind a config:

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker

Recreate from that file in three deliberate steps. First tear down the old named cluster so you do not fight a leftover single-node layout:

kind delete cluster --name geekmonks-lab

Create again, pointing kind at the config (1 control-plane + 2 workers):

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

List nodes wide to see workers and their INTERNAL-IP / placement columns:

kubectl get nodes -o wide

Example wide output:

NAME                          STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION
geekmonks-lab-control-plane   Ready    control-plane   1m    v1.32.x   172.18.0.2    <none>        Debian GNU…    …
geekmonks-lab-worker          Ready    <none>          1m    v1.32.x   172.18.0.3    <none>        Debian GNU…    …
geekmonks-lab-worker2         Ready    <none>          1m    v1.32.x   172.18.0.4    <none>        Debian GNU…    …

Workers show Ready without the control-plane role. That is enough topology for placement experiments without a cloud account.

Smoke deploy: prove the cluster works

Prove the API can schedule and expose a tiny app. Start with a dedicated namespace so lab objects stay out of default:

kubectl create namespace lab

Create a Deployment that runs two nginx replicas (controller keeps the count):

kubectl -n lab create deployment web --image=nginx:1.27 --replicas=2

Expose that Deployment with a ClusterIP Service so other pods can reach it by DNS inside the cluster:

kubectl -n lab expose deployment web --port=80 --target-port=80

Take one health snapshot — Deployment Ready counts, pod placement, and the Service IP:

kubectl -n lab get deploy,pods,svc -o wide

Healthy shape looks like:

NAME                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web   2/2     2            2           30s

NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE
pod/web-7d9f8c6b5-xk2m4   1/1     Running   0          30s   10.244.1.4   geekmonks-lab-worker
pod/web-7d9f8c6b5-p9q1r   1/1     Running   0          30s   10.244.2.5   geekmonks-lab-worker2

NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
service/web   ClusterIP   10.96.100.10   <none>        80/TCP    30s

READY 2/2 on the Deployment and 1/1 on each pod is the deploy-health signal. Reach a pod from another temporary pod if you want a quick network check:

kubectl -n lab run curl --rm -it --image=curlimages/curl --restart=Never -- \
  curl -sS -o /dev/null -w "%{http_code}\n" http://web.lab.svc.cluster.local/

Typical result:

200

Note: STATUS can say Running while READY is still 0/1 during image pulls. Wait for Ready counts before declaring the lab broken.

When you want a deeper inspect pass on what landed — labels, images, events — continue with kubectl: See What Is Deployed and How It Is Configured.

Habits that save time

Load a local image into kind

Images you build on the host are not automatically visible inside kind nodes. After docker build -t myapp:dev ., copy that image into the named cluster’s node containers:

kind load docker-image myapp:dev --name geekmonks-lab

Point the Deployment at the tag you just loaded so new pods use it:

kubectl -n lab set image deployment/web web=myapp:dev

Otherwise pods sit in ImagePullBackOff looking for a registry that does not have your laptop tag.

Keep labs named and small

Prefer --name geekmonks-lab (or a ticket id) over the default cluster name when you juggle more than one. Delete aggressively; recreate is cheap.

Know what kind is not

Do not treat kind Ingress, LoadBalancer behavior, or storage classes as identical to EKS/GKE/AKS. Learn the API and controllers here; validate cloud-specific wiring in a cloud sandbox when that is the real question.

Tear down cleanly

When the POC is done:

kind delete cluster --name geekmonks-lab

List kubeconfig contexts and look for a leftover kind-geekmonks-lab entry (delete can leave stale rows):

kubectl config get-contexts

Confirm no kind node containers for that lab remain on Docker:

docker ps --filter "name=geekmonks-lab"

Note: Deleting the cluster removes the nodes. Stale kubeconfig entries can remain until you clean them (kubectl config delete-context kind-geekmonks-lab and related cluster/user entries). A leftover context pointing at a dead API is a common “connection refused” surprise next week.

Quick reference card

GoalCommand
Create named clusterkind create cluster --name geekmonks-lab
Create from configkind create cluster --name geekmonks-lab --config kind-config.yaml
Current contextkubectl config current-context
Nodeskubectl get nodes -o wide
Smoke deploykubectl -n lab create deployment web --image=nginx:1.27
Load local imagekind load docker-image IMAGE:TAG --name geekmonks-lab
Delete clusterkind delete cluster --name geekmonks-lab

Practice drills

Use a machine with Docker and kind. The point is to choose the next command with intent. Answers sit under each step — solid shapes, not the only ones.

  1. Create a named cluster and print the current kubectl context.

    kind create cluster --name geekmonks-lab
    kubectl config current-context
  2. Recreate the same name as a multi-node cluster from a config file and list nodes wide.

    kind delete cluster --name geekmonks-lab
    kind create cluster --name geekmonks-lab --config kind-config.yaml
    kubectl get nodes -o wide
  3. Deploy two nginx replicas in namespace lab and confirm Deployment Ready is 2/2.

    kubectl create namespace lab
    kubectl -n lab create deployment web --image=nginx:1.27 --replicas=2
    kubectl -n lab get deploy web
  4. Load a local image tag into the cluster (build a tiny image first if needed).

    kind load docker-image myapp:dev --name geekmonks-lab
  5. Delete the cluster and confirm no geekmonks-lab containers remain.

    kind delete cluster --name geekmonks-lab
    docker ps --filter "name=geekmonks-lab"

If you can work through those five comfortably, you already cover the local-lab loop: stand up kind, verify context and nodes, smoke-test with a Deployment, load images when the registry is “your laptop,” and tear down without leaving a ghost context. Start with a named disposable cluster, prove Ready counts before debugging YAML, and move to kubectl inspection when you need to read what the cluster actually scheduled.

Next optional step See what is deployed and how it is configured. kubectl: See What Is Deployed and How It Is Configured