Most engineers pick a local Kubernetes tool once, never revisit the decision, and then spend the rest of their time wondering why their local setup doesn't behave the way the docs say it should.
The answer is usually the tool. minikube runs a VM with a fixed memory allocation that you can't easily change and that starts automatically at boot. Docker Desktop's built-in Kubernetes shares a daemon with your other containers and can't be scripted. k3d is fast and lightweight — but it runs k3s, a non-standard distribution, which means when you reach for Cilium installation docs or the Envoy Gateway quickstart, you're following instructions written for a different runtime.
kind — Kubernetes IN Docker — runs real kubeadm inside Docker containers. It's what upstream Kubernetes CI uses. It's what Cilium's official installation guide targets. It's what the Envoy Gateway quickstart runs against. When something doesn't work, the debugging trail leads somewhere useful.
This is the first post in the Local Platform Engineering Series. We start here because everything that follows — Gateway API routing, LGTM observability, Cilium networking, LLM inference monitoring, AI tool gateways — runs on top of this cluster.
Local Platform Engineering Series
- Running Local Kubernetes with kind: Ephemeral by Design, Production-Honest by Default
- Gateway API in Practice: From Ingress Migration to Envoy Debugging
- Multi-Tenant Observability: LGTM at Platform Scale
- Network Control with Cilium and Kyverno: Policies That Actually Work
- Observing LLM Inference: The Metrics That Actually Matter
- AI Tool Gateways: Sandboxing Agent Access in Kubernetes
What kind Actually Does
kind creates Docker containers that run Kubernetes node processes. The control-plane container runs the API server, scheduler, and controller manager. Worker containers run kubelet and containerd. kubeadm wires them together — exactly the same way a real cluster is bootstrapped.
The cluster has no persistent state outside those containers. Delete the containers, the cluster is gone. Create them again, you have a clean cluster. The entire lifecycle is a single command in each direction.
This is deliberately not a VM. There's no hypervisor overhead, no fixed memory allocation that you set once and regret, no background daemon holding resources while you're not using Kubernetes. Docker manages the containers. When you're not running the cluster, the containers are stopped. When you delete the cluster, they're removed.
The tradeoff: kind uses kubeadm and runs a full etcd, API server, and controller stack. It's heavier than k3s-based tools. On a machine with 16 GB RAM, this is irrelevant. On a machine with 8 GB that's also running Chrome, IntelliJ, and a local database, you'll feel it. k3d is the right choice in that situation. For the stack we're building in this series — Cilium, the LGTM observability stack, Ollama — you need the headroom either way.
Installation
brew install kind kubectl helm
kind version # ≥ 0.23.0brew install kind kubectl helm
kind version # ≥ 0.23.0Docker Desktop must be running. Set at least 8 GB RAM in Docker Desktop → Settings → Resources before proceeding. The LGTM stack alone needs 4 GB; leave headroom for your cluster nodes and Ollama.
The Cluster Config
Write a config file. You'll recreate this cluster many times — inline flags don't commit, configs do.
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: platform-local
nodes:
- role: control-plane
- role: worker
- role: workerkind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: platform-local
nodes:
- role: control-plane
- role: worker
- role: workerThree nodes: one control-plane, two workers. Two workers matter when you're testing pod anti-affinity, topology spread constraints, or anything that cares about node distribution. One worker hides those issues.
kind ships with a local-path storage provisioner and kindnet as the default CNI. Both work out of the box. In part four, we'll replace kindnet with Cilium — but that's a separate step with its own config changes. Start with this.
kind create cluster --config kind-config.yaml
kubectl config use-context kind-platform-local
kubectl get nodeskind create cluster --config kind-config.yaml
kubectl config use-context kind-platform-local
kubectl get nodesThree nodes, all Ready, in around 30 seconds.
Day-to-Day Workflow
Load local images
kind nodes use their own containerd runtime, separate from Docker's image store. Build locally, then import:
docker build -t my-service:dev .
kind load docker-image my-service:dev --name platform-localdocker build -t my-service:dev .
kind load docker-image my-service:dev --name platform-localReference it in manifests with imagePullPolicy: Never. The image is already on the nodes; no registry pull happens.
Reset cleanly
When a cluster accumulates state you don't want — botched Helm releases, conflicting CRDs, half-applied manifests — delete and recreate:
kind delete cluster --name platform-local
kind create cluster --config kind-config.yamlkind delete cluster --name platform-local
kind create cluster --config kind-config.yamlThirty seconds. This is the workflow, not a fallback. The config is committed; recreating costs nothing. Don't debug a cluttered cluster when a clean one takes half a minute.
Multiple clusters
kind runs multiple clusters concurrently, each as an isolated set of Docker containers:
kind create cluster --config kind-config.yaml
kind create cluster --name platform-local-secondary
kubectx # switch between themkind create cluster --config kind-config.yaml
kind create cluster --name platform-local-secondary
kubectx # switch between themUseful for testing cross-cluster federation, multi-cluster service discovery, or demonstrating failover behaviour without spinning up any cloud infrastructure.
kind vs k3d vs minikube
These tools solve different problems. Be deliberate about which one you're using and why:
| kind | k3d | minikube | |
|---|---|---|---|
| Startup time | ~30s | ~20s | ~90s (VM) |
| Memory overhead | Medium (kubeadm) | Low (k3s) | High (VM) |
| Default CNI | kindnet (NetworkPolicy ✓) | flannel (NetworkPolicy ✗) | kindnet |
| Upstream k8s CI | Yes | No | No |
| Cilium docs target | Yes | Partial | No |
| Gateway API quickstart | Yes | No | No |
| Production similarity | kubeadm | k3s | kubeadm |
| Image loading | kind load docker-image | k3d image import | minikube image load |
Use k3d when memory is genuinely constrained and you don't need Cilium or anything that specifically targets kind. Use minikube when you need GPU passthrough or VM-level networking isolation. Use kind for everything in this series — the ecosystem alignment alone is worth the 10-second startup difference.
The reason the Cilium guide and Envoy Gateway quickstart are written for kind is not coincidence. It's because kind's kubeadm setup is close enough to production that the same config works. k3s has enough differences in the control plane, CNI configuration, and node labelling that you hit edge cases the docs don't cover.
Teardown
kind delete cluster --name platform-localkind delete cluster --name platform-localNothing left. No background daemon. No disk image. No lingering network interface. The Docker containers are removed, and Docker reclaims the resources.
If you're done for the day but want the cluster back tomorrow, stop it instead:
docker stop $(docker ps -q --filter "name=platform-local")
# resume
docker start $(docker ps -aq --filter "name=platform-local")docker stop $(docker ps -q --filter "name=platform-local")
# resume
docker start $(docker ps -aq --filter "name=platform-local")kind clusters survive Docker restarts this way. Everything in the cluster is preserved — deployments, ConfigMaps, PersistentVolumes. The cluster picks up where it left off.
What's Next
A cluster with no routing layer means port-forwarding everything. Part two sets up the Kubernetes Gateway API with Envoy Gateway — host-based routing across all services in the cluster, the same pattern a real platform would use.
Gateway API in Practice: From Ingress Migration to Envoy Debugging →