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?”:
| Tool | Best fit | Trade-off |
|---|---|---|
| kind | Local labs, CI, multi-node experiments | Needs Docker (or a compatible runtime); networking is container-on-container |
| minikube | Classic laptop cluster, many drivers | Heavier footprint; more “product” surface than you need for a quick POC |
| k3d | Fast k3s-in-Docker labs | k3s 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.
| Path | Examples | When teams pick it |
|---|---|---|
| Managed cloud Kubernetes | Amazon EKS, Google GKE, Azure AKS | Shared responsibility, cloud IAM/networking, least ops toil for most product teams |
| Self-managed / bootstrap | kubeadm, kops, Cluster API | Full control of control-plane and node lifecycle on your own (or hybrid) infrastructure |
| Platform distributions | OpenShift, Rancher-managed clusters | Opinionated 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:
- A container runtime — Docker Desktop, Docker Engine, or a compatible runtime kind supports
kubectl— talks to the API server kind wires into your kubeconfigkind— 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
| Goal | Command |
|---|---|
| Create named cluster | kind create cluster --name geekmonks-lab |
| Create from config | kind create cluster --name geekmonks-lab --config kind-config.yaml |
| Current context | kubectl config current-context |
| Nodes | kubectl get nodes -o wide |
| Smoke deploy | kubectl -n lab create deployment web --image=nginx:1.27 |
| Load local image | kind load docker-image IMAGE:TAG --name geekmonks-lab |
| Delete cluster | kind 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.
-
Create a named cluster and print the current kubectl context.
kind create cluster --name geekmonks-lab kubectl config current-context -
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 -
Deploy two nginx replicas in namespace
laband confirm Deployment Ready is2/2.kubectl create namespace lab kubectl -n lab create deployment web --image=nginx:1.27 --replicas=2 kubectl -n lab get deploy web -
Load a local image tag into the cluster (build a tiny image first if needed).
kind load docker-image myapp:dev --name geekmonks-lab -
Delete the cluster and confirm no
geekmonks-labcontainers 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.