Run a Multi-Node Kubernetes Cluster Locally with kind
Spin up a three-node Kubernetes cluster in Docker with kind, then deploy and expose nginx with kubectl.
What you'll build
A three-node Kubernetes cluster (one control plane, two workers) running entirely in Docker containers on your laptop, with an nginx deployment exposed to your host at http://localhost:8080. Total time: about ten minutes, most of it image pulls.
Prerequisites
Verified September 2026 against kind v0.33.0, kubectl v1.37, and the kindest/node:v1.37.0 image.
- macOS or Linux. On Windows, run everything inside WSL2.
- Docker Desktop (macOS) or Docker Engine (Linux), running. kind also works with Podman and nerdctl, but this tutorial assumes Docker.
- At least 4 GB of RAM available to Docker. Three nodes means three fat containers.
- No cloud account, no VMs. Everything is local.
1. Install kind and kubectl
On macOS, Homebrew handles both:
brew install kind kubectl
On Linux (amd64), grab the binaries directly:
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.33.0/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x ./kubectl && sudo mv ./kubectl /usr/local/bin/kubectl
Confirm versions with kind version and kubectl version --client. You want kind v0.33.0 and a kubectl within one minor version of v1.37.
2. Define a multi-node cluster
kind create cluster with no arguments gives you a single node. A config file gets you real multi-node scheduling. Save this as kind-config.yaml:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 8080
protocol: TCP
- role: worker
- role: worker
The extraPortMappings block publishes port 30080 on the control-plane container to port 8080 on your host, the same way docker run -p would. You'll point a NodePort service at 30080 later; kind requires the service's nodePort and the mapping's containerPort to match.
3. Create the cluster
kind create cluster --name dev --config kind-config.yaml
First run pulls the ~1 GB node image, so expect a few minutes. kind writes the cluster's credentials into your kubeconfig and switches your current context to kind-dev (kind prefixes every cluster name with kind-). Check the nodes:
kubectl get nodes
All three should reach Ready within a minute or two. Under the hood, each "node" is a Docker container running kubeadm-bootstrapped Kubernetes; docker ps will show dev-control-plane, dev-worker, and dev-worker2.
4. Deploy an app
kubectl create deployment web --image=nginx:1.29
kubectl scale deployment web --replicas=3
Watch the scheduler spread the pods across your workers:
kubectl get pods -o wide
The NODE column shows placement, which is the payoff of multi-node: you can watch rescheduling, rolling updates, and node failures behave like they would on a real cluster.
5. Expose it
kubectl expose can't pin a specific nodePort, so use a manifest. Save as service.yaml:
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort
selector:
app: web
ports:
- port: 80
targetPort: 80
nodePort: 30080
kubectl apply -f service.yaml
The service opens port 30080 on every node; the port mapping from step 2 forwards your host's 8080 into it.
6. Iterate
Ship a new image version and watch the rollout, no rebuild or reprovisioning:
kubectl set image deployment/web nginx=nginx:1.29-alpine
kubectl rollout status deployment/web
When you're done for the day, kind delete cluster --name dev removes everything.
Verify it works
curl -s http://localhost:8080 | grep '<title>'
Expected output:
<title>Welcome to nginx!</title>
And the cluster itself:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
dev-control-plane Ready control-plane 3m v1.37.0
dev-worker Ready <none> 2m v1.37.0
dev-worker2 Ready <none> 2m v1.37.0
Troubleshooting
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? during kind create cluster means exactly what it says. Start Docker Desktop, or on Linux sudo systemctl start docker.
ERROR: failed to create cluster: node(s) already exist for a cluster with the name "dev" means a previous cluster is still around. Run kind delete cluster --name dev and recreate.
Bind for 0.0.0.0:8080 failed: port is already allocated means something else on your host owns 8080. Change hostPort in kind-config.yaml to a free port and recreate the cluster; port mappings can't be edited on a live cluster.
Pods stuck crash-looping with failed to create fsnotify watcher: too many open files (Linux only) is a known kind issue with multi-node clusters hitting inotify limits. Fix:
sudo sysctl fs.inotify.max_user_watches=524288
sudo sysctl fs.inotify.max_user_instances=512
Next steps
Load locally built images straight into the cluster with kind load docker-image myapp:dev, which skips the registry round-trip entirely. After that, add an ingress controller so you can route by hostname instead of NodePorts, and try kind create cluster --image kindest/node:v1.36.4 to test your manifests against older Kubernetes versions before an upgrade.
Sources & further reading
- kind Quick Start — kind.sigs.k8s.io
- kind Configuration — kind.sigs.k8s.io
- kind v0.33.0 release notes — github.com
- Install and Set Up kubectl on macOS — kubernetes.io
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 4
kind is solid for local testing. just wish it was faster to bootstrap, but beats spinning up vms every time
kind is great, but the first time i had to debug a networking issue inside those containers and realized the node IPs shift on every restart, i spent a day building a local etcd-backed setup with proper DNS. turned out i just needed to read the kind docs on persistent storage and load balancer config. saves a lot of grief if you're testing anything stateful.
yeah, that's the gap that catches people. kind is friction-free until it isn't, and then you're deep in container networking. persistent storage config could use better visibility in the quick-start docs tbh
yeah, that's the gap nobody mentions. kind works until it doesn't, then you're deep in container networking hell. good point about the docs though—most people probably skip straight to stackoverflow.