kind (Kubernetes in Docker)
kind runs a full Kubernetes cluster on your laptop by turning ordinary Docker containers into Kubernetes nodes — one container is the control plane, more containers are workers, and kubeadm bootstraps each one exactly as it would a real machine. It was built by Kubernetes SIG Testing to run the project's own upstream conformance suite without needing cloud credentials or real hardware, and that origin still shows: a fresh multi-node cluster comes up in under a minute, and tearing it down leaves nothing behind but a stopped container. Today it's the default way most engineers get a disposable, close-to-real cluster for local development, and it's just as commonly the cluster CI pipelines create fresh on every run to execute a Helm chart, an integration test, or a policy check against real API objects instead of a mock. It is not a production runtime, and it doesn't try to be — knowing exactly where that line sits is most of what this page is about.
Imagine you want to practice being mayor of a whole town, but building a real town is expensive and slow. So instead you set up a row of cardboard boxes on the kitchen table — one box says "City Hall" on it, a few others say "Fire Station" and "School." Each box acts like a real building well enough to practice with: you can walk your toy people between them, knock one down and watch what happens to the town, add a new box for a new building. When you're done practicing, you fold the boxes flat and the kitchen table is clean again — nothing was ever really built, so nothing real has to be un-built. kind is the box trick for Kubernetes: each cardboard box is a Docker container standing in for a whole computer, good enough to practice the real thing on, gone the instant you're done.
What kind actually is, and why kubeadm is underneath it
☺ Like you're 10: Each container pretends to be a whole separate computer, and the same setup script real machines use — kubeadm — is what turns each pretend computer into a real Kubernetes node.
A kind "node" is a single Docker container running a specially built image, kindest/node, that carries a systemd init process, a container runtime (containerd), and the Kubernetes binaries for one specific version — the image tag is the Kubernetes version, e.g. kindest/node:v1.31.0. On cluster creation, kind picks one container to be the control-plane node and runs kubeadm inside it exactly the way you'd run it on bare metal, then runs kubeadm join inside each worker container to attach it. This is the detail that makes kind trustworthy rather than a toy: nothing about the API server, the scheduler, or kube-proxy is faked or reimplemented — you're running the genuine upstream control plane described in Kubernetes Architecture, just with node isolation coming from Docker's namespaces and cgroups instead of separate physical or virtual machines.
All of a kind cluster's containers sit on one Docker bridge network and can reach each other by container name, which is also how a control-plane container reaches a worker to schedule a Pod onto it. Docker Desktop, Docker Engine on Linux, and (experimentally, via KIND_EXPERIMENTAL_PROVIDER=podman) Podman can all host a kind cluster — the only hard requirement is a working container runtime with enough CPU and memory handed to it, since every node you ask for draws from that same pool.
Every node shares the CPU, memory, and kernel of one Docker host, container-to-container networking has none of a real CNI's cross-machine routing to prove out, and the whole cluster disappears the moment you run kind delete cluster or reboot the host with no persistence configured. kind is honest about this in its own docs. Treat it as a fast, disposable stand-in for local development, CI, and learning — never as somewhere a real workload runs.
The kind config file: multi-node, custom CNI, ingress
☺ Like you're 10: One YAML file says how many pretend computers you want, which jobs they each do, and which doors on your real computer should open into them.
A one-line kind create cluster gives you a single all-in-one node, fine for a quick smoke test. Everything past that goes in a kind.x-k8s.io/v1alpha4 Cluster config, passed with --config, which is where multi-node topology, a swapped-out CNI, and ingress-ready port mappings all live:
# kind-config.yaml — 1 control plane, 2 workers, custom CNI, ingress ports
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: dev
networking:
disableDefaultCNI: true # skip kindnet — you'll install Calico/Cilium yourself
podSubnet: "10.244.0.0/16" # must match what your CNI manifest expects
serviceSubnet: "10.96.0.0/16"
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings: # host → this node's container, for ingress-nginx
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
- role: worker
containerdConfigPatches: # route a private registry through localhost:5001
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5001"]
endpoint = ["http://kind-registry:5000"]kubeadmConfigPatches and kubeadmConfigPatchesJSON6902 reach straight into the kubeadm InitConfiguration/ClusterConfiguration objects kind generates for you — the same shape you'd hand-write for a real cluster, covered in the kubeadm tool guide. That's how the control-plane node above gets labeled ingress-ready=true: kindnet and the stock config don't know or care about ingress controllers, so the label is how ingress-nginx's kind-specific manifest later finds the right node to schedule onto.
CNI and ingress in a kind cluster
☺ Like you're 10: kind comes with a simple built-in network already working, but you can swap it out for a real one if that's what you're actually testing.
By default every kind cluster ships kindnet, a minimal CNI good enough for Pod-to-Pod connectivity and not much else — no NetworkPolicy enforcement, no BGP, none of the eBPF dataplane features a real deployment might depend on. That's fine for testing a Deployment or a Helm chart, and it's exactly why disableDefaultCNI: true exists: set it, then apply a real CNI manifest — Calico or Cilium — right after cluster creation, matching its expected pod CIDR to your podSubnet. This is the standard way Networking & CNI and Multi-Cluster & Fleet Management both recommend proving out NetworkPolicy behavior or multi-cluster mesh federation without touching a cloud account.
Ingress works the same deliberate way: nothing routes traffic from your laptop into the cluster until you wire it up. The pattern above — one node labeled ingress-ready=true plus extraPortMappings for 80 and 443 on that same node — is exactly what ingress-nginx's official kind deployment manifest expects: it carries a nodeSelector for that label and a toleration for the control-plane taint, so it schedules onto the one node whose container actually has those host ports mapped out. Once that's applied, curl localhost/ on your own machine reaches an Ingress object inside the cluster, no port-forward required.
Nothing above touches type: LoadBalancer. As Services & Networking covers, EXTERNAL-IP only leaves <pending> when something is actively watching for LoadBalancer Services and provisioning for them — a cloud controller manager on a real cloud, or on kind specifically, the community cloud-provider-kind project (or MetalLB) if you install it yourself. Out of the box, a kind cluster has no LoadBalancer implementation at all; that's expected, not broken.
Day-to-day commands
☺ Like you're 10: A handful of commands cover almost everything: make a cluster, get an image into it, look inside it, throw it away.
The one command that trips people up first: kind clusters don't automatically see images sitting in your local Docker cache the way you might expect, because each node container has its own isolated containerd image store. kind load is how an image gets from your machine into every node's store without going through a registry at all.
# --- create, inspect, and remove clusters --- $ kind create cluster --config kind-config.yaml # named "dev" per the config above $ kind get clusters # every kind cluster on this host $ kind get nodes --name dev # the container name of every node $ kubectl config get-contexts # kind writes/merges "kind-dev" for you $ kind delete cluster --name dev # gone — containers stopped and removed # --- get a locally built image INTO the cluster, no registry needed --- $ docker build -t myapp:dev . $ kind load docker-image myapp:dev --name dev # pushes into every node's containerd store $ kind load image-archive myapp.tar --name dev # same, from a saved tarball (airgapped CI) # --- look inside a node like it's a real machine --- $ docker exec -it dev-control-plane crictl ps # containerd's CLI, not docker ps $ docker exec -it dev-worker journalctl -u kubelet -f # kubelet's systemd logs, live # --- capture cluster state before tearing it down --- $ kind export logs ./kind-logs --name dev # every component's logs, pre-delete $ kind export kubeconfig --name dev # re-merge if the context ever goes stale
That last habit — kind export logs right before kind delete cluster — is worth building early. Once a cluster is gone its logs are gone with it; there's no "kubectl logs on a container that no longer exists."
Using kind in CI
☺ Like you're 10: Every test run gets its own brand-new cluster, used once, then thrown away — so one run's mess can never leak into the next.
This is where kind earns most of its keep in practice. A CI runner is already an ephemeral, throwaway environment, and a kind cluster fits that model exactly: create it fresh at the start of the job, run real kubectl and Helm commands against a real API server, delete it (or just let the runner get recycled) at the end. No shared cluster means no state from one pull request's test run can contaminate the next one's — the single biggest source of "works on my machine, flakes in CI" that a shared staging cluster tends to accumulate.
# .github/workflows/test.yml — matrix-tested against three Kubernetes minors
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
matrix:
k8s-version: ["v1.29.8", "v1.30.4", "v1.31.0"]
steps:
- uses: actions/checkout@v4
- uses: helm/kind-action@v1
with:
node_image: kindest/node:${{ matrix.k8s-version }}
config: ./ci/kind-config.yaml
cluster_name: ci
- run: docker build -t myapp:${{ github.sha }} .
- run: kind load docker-image myapp:${{ github.sha }} --name ci
- run: helm upgrade --install myapp ./chart
--set image.tag=${{ github.sha }} --wait --timeout 90s
- run: kubectl wait --for=condition=available deploy/myapp --timeout=60s
- run: ./run-integration-tests.sh
- if: failure()
run: kind export logs ./kind-logs --name ci # the artifact worth uploading on redThe node_image matrix above is the other reason CI teams reach for kind specifically: kindest/node images are published and pinned per Kubernetes minor version, so testing a Helm chart or an admission webhook against three supported Kubernetes versions in parallel is a one-line matrix, not three separately provisioned cloud clusters. It's the same trick that lets kind test Kubernetes itself against every minor it still supports.
Gotchas and limits
☺ Like you're 10: A few things catch almost everyone once: images that "aren't there," storage that vanishes, and a real exam that isn't run this way at all.
Images don't cross the boundary by themselves. Covered above, but worth repeating because it's the single most common first-timer confusion: docker build populates your host's Docker image cache, not any node's containerd store, and a Pod referencing that image with no registry behind it will sit in ImagePullBackOff until you run kind load docker-image. Setting imagePullPolicy: Never or IfNotPresent on the Deployment matters too — the default Always will try to pull from a registry even for an image kind just loaded locally.
Storage doesn't survive the cluster. kind ships a default StorageClass backed by Rancher's local-path-provisioner, which is genuinely useful for testing a StatefulSet's PVC lifecycle end to end — see Storage & CSI — but the actual bytes live inside the node container's own filesystem. kind delete cluster takes every PersistentVolume's data with it; there's no separate disk to reattach to a new cluster.
Resource pressure isn't representative. Every node container draws CPU and memory from the same Docker host pool, so a five-node kind cluster on a laptop with 8 vCPUs doesn't behave like five real machines with 8 vCPUs each — it behaves like one machine's worth of resources, sliced five ways, contending with whatever else is running on your laptop at the time. Don't trust eviction behavior, scheduler pressure, or resource-request tuning you observed on kind to transfer directly to real hardware; Scheduling & Resource Management is explicit about that gap.
It isn't the CKA/CKAD/CKS exam environment. Those exams run against a pre-provisioned remote cluster you reach through a browser-based terminal — you don't build the cluster yourself, and kind isn't installed there. That doesn't make kind a bad way to prepare; the API objects, kubectl behavior, and failure modes you practice against a kind cluster are the genuine upstream Kubernetes ones. It just means "set up a kind cluster" is never itself an exam task. This course's own CKA practice tasks and CKAD practice tasks are written to run on exactly this kind of throwaway cluster.
kind vs. minikube vs. k3d: picking a local cluster tool
☺ Like you're 10: They all give you a practice cluster — they just disagree about what a "node" pretends to be.
All three exist to solve the same problem — a real, standards-conformant Kubernetes API to develop and test against, without a cloud bill — and the honest answer for which to reach for is usually "whichever one your team already standardized on," because the differences are real but narrow.
| Dimension | kind | minikube | k3d |
|---|---|---|---|
| What a "node" is | A Docker container running full upstream kubeadm-bootstrapped Kubernetes | Typically one VM (or container driver), a more complete single-machine simulation | A Docker container running k3s, Rancher's trimmed-down Kubernetes distribution |
| Multi-node by default | Yes — the whole point of its config file | Supported, less commonly used that way | Yes, similarly config-driven |
| Extras included | None — deliberately minimal, you install what you test | Addons: dashboard, metrics-server, ingress, registry, one command each | Whatever k3s bundles (Traefik, local-path, CoreDNS) unless disabled |
| Startup speed | Fast — seconds to low tens of seconds | Slower with a VM driver; comparable with a container driver | Fast — comparable to kind, sometimes faster given k3s's smaller footprint |
| Closest to "real" upstream Kubernetes | Yes — same binaries, same kubeadm path | Yes, with the Docker/container driver; VM drivers add a layer | No — k3s trims and swaps several components for size |
| Where it shines | CI matrices, conformance-style testing, exam practice | A single richer local dev box with batteries included | Very fast, very light iterative local dev |
The practical rule most teams land on: reach for kind when you want the closest thing to real upstream Kubernetes behavior for CI or certification practice, reach for minikube when you want a friendlier single-machine dev experience with common addons a command away, and reach for k3d when startup speed and a tiny footprint matter more than being byte-for-byte identical to a stock control plane. Nothing stops a team from using more than one — kind in CI, minikube on individual laptops — since all three speak the same Kubernetes API underneath.
Save the kind-config.yaml from earlier on this page and run kind create cluster --config kind-config.yaml. Confirm you got three nodes with kubectl get nodes, then apply Calico's manifest and watch every node reach Ready only after its CNI Pod does. Build a tiny "hello" container image locally, kind load docker-image it in, and deploy it with imagePullPolicy: Never — watch it start instantly with no registry involved. Then install ingress-nginx's kind manifest, create an Ingress for your hello app, and hit curl localhost/ from your own terminal. Finally, run kind export logs ./logs, open the folder, and see just how much every component was already logging that kubectl alone never shows you.
"People ask me why I don't just keep one kind cluster running all week and reuse it. Because reuse is exactly the risk — a stale ConfigMap from Tuesday's test, a CRD nobody cleaned up, a NetworkPolicy left over from a debugging session, and suddenly Friday's 'passing' test run was only passing against Tuesday's leftovers, not against what the manifest actually says today. kind create, run the thing, kind delete. Every single time. The half-second I save by keeping a cluster warm isn't worth the afternoon I'd lose chasing a bug that only exists because of state I forgot was there."
Remy the Rabbit: Cluster up, chart installed, tests green, cluster gone — forty seconds, start to finish. Next.
Recon the Robot: I clocked it at forty-one. I'm not complaining. A CI job that starts from zero every single time is the only kind of reconcile loop I actually trust completely — nothing carried over for me to be wrong about.
Gizmo: Or — hear me out — we just keep one kind cluster running forever on the build server. Skip the create step entirely! Think of the seconds we'd save!
Timmy the Turtle: The seconds you'd save today are the afternoon you'd lose next month, when a test passes because of a Secret somebody left in that cluster in March and nobody remembers applying. Ephemeral isn't slower, Gizmo. It's honest.
Foxy: And when it does break, it breaks the same way it would on a real cluster — real kubeadm, real API server. Half my troubleshooting instincts got built practicing on exactly this.
Ellie the Elephant: Just don't confuse fast-and-disposable with production-grade. I've watched one too many "it worked fine on kind" statements walk straight into a LoadBalancer stuck at pending, on a cluster that actually needed a cloud controller to answer.
Remy: Fair. Disposable practice ground, not a home. I'll take that trade for forty-one seconds every time.
1. What is a kind "node" actually made of, and which real bootstrapping tool runs inside it to turn a plain container into a working Kubernetes node? 2. You build an image locally and reference it in a Deployment, and the Pod sits in ImagePullBackOff. What almost certainly went wrong, and what two things fix it? 3. Why does disableDefaultCNI: true exist, and what has to match between your config and the CNI manifest you apply afterward for Pod networking to actually come up? 4. A Service of type: LoadBalancer sits at <pending> on a fresh kind cluster. Is that a bug — and what would you install to change it? 5. Why is deleting and recreating a kind cluster for every CI run usually the right call, rather than reusing one long-lived cluster? 6. Is a kind cluster ever the actual CKA or CKAD exam environment — and if not, why is practicing on one still worthwhile?
Check your answers
- A kind node is a Docker (or Podman) container running the
kindest/nodeimage, which bundles containerd and the Kubernetes binaries;kubeadmruns inside that container —kubeadm initon the control-plane container,kubeadm joinon each worker — exactly as it would on a real machine. - Almost always a missing image:
docker buildonly populates your host's Docker cache, not any node container's isolatedcontainerdstore. Fix it by runningkind load docker-image <image> --name <cluster>to push it into every node, and setimagePullPolicy: Never(orIfNotPresent) so the kubelet doesn't try to pull it from a registry that doesn't have it. - kind's default CNI, kindnet, is deliberately minimal — no NetworkPolicy enforcement and none of a real CNI's advanced features — so
disableDefaultCNI: truelets you install Calico, Cilium, or another CNI to test against instead. The CNI manifest's expected pod CIDR has to match thepodSubnetset in the kind config, or Pod networking won't come up correctly. - Not a bug — kind ships with no LoadBalancer implementation at all out of the box, so
EXTERNAL-IPstays<pending>until something is actively provisioning for it. Installing the communitycloud-provider-kindproject or MetalLB gives LoadBalancer Services a real external IP on a kind cluster. - A fresh cluster per run guarantees no state — a leftover ConfigMap, CRD, or NetworkPolicy from a previous run — can make a test pass or fail for the wrong reason. Reusing one long-lived cluster trades a few seconds of setup time for exactly the kind of hidden, hard-to-reproduce state drift CI exists to prevent.
- No — CKA, CKAD, and CKS run on a pre-provisioned remote cluster reached through a browser-based terminal; you never build the cluster yourself, and "set up kind" is never an exam task. It's still worth practicing on, because kind runs the genuine upstream API server, scheduler, and kubelet behavior — the actual skills the exam tests transfer directly, even though the environment you build them on differs from the one you sit the exam in.