Kind vs Minikube
No VM Overhead
Kind's key architectural difference from Minikube is that there is no hypervisor involved. Each Kubernetes node is a Docker container running a custo image (kindest/node) that bundles containerd, kubelet, and all necessary binaries. This means:
- Startup time: Kind creates a cluster in 20–40 seconds vs 60–120 seconds for Minikube with a VM driver
- Resource usage: No hypervisor memory overhead; each node container uses only what it needs
- No driver selection: Kind always uses Docker (or Podman) — no driver configuration needed
- Reproducibility: Container-based nodes start deterministically, making Kind ideal for CI where VMs cause flakiness
CI Use Case
Kind was purpose-built for testing Kubernetes controllers and operators. The typical CI workflow: spin up cluster → build image → load into Kind → deploy → assert → delete cluster. Because Docker is available in virtually every CI runner (GitHub Actions, GitLab CI, CircleCI, Jenkins), Kind works out-of-the-box with no special runner configuration.
Installation
macOS & Linux
# macOS via Homebrew
brew install kind
# Linux: download binary (amd64)
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
# Linux: via Go (if you have Go 1.21+)
go install sigs.k8s.io/kind@v0.23.0
# Verify
kind version
# kind v0.23.0 go1.21.10 linux/amd64
# kubectl is also needed (if not already installed)
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
Windows
# Chocolatey
choco install kind
# Scoop
scoop install kind
# Winget
winget install Kubernetes.kind
# Manual: PowerShell (amd64)
curl.exe -Lo kind-windows-amd64.exe https://kind.sigs.k8s.io/dl/v0.23.0/kind-windows-amd64
Move-Item .\kind-windows-amd64.exe C:\Windows\kind.exe
# Verify
kind version
Your First Cluster
Create & Connect
# Create a cluster with the default name 'kind'
kind create cluster
# Creating cluster "kind" ...
# ✓ Ensuring node image (kindest/node:v1.30.2) 🖼
# ✓ Preparing nodes 📦
# ✓ Writing configuration 📜
# ✓ Starting control-plane 🕹️
# ✓ Installing CNI 🔌
# ✓ Installing StorageClass 💾
# Set kubectl context to "kind-kind"
# You can now use your cluster with: kubectl cluster-info --context kind-kind
# Create with a specific name
kind create cluster --name my-project
# Create with a specific Kubernetes version (matches kindest/node image tag)
kind create cluster --name versioned --image kindest/node:v1.29.4
# Verify nodes
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# kind-control-plane Ready control-plane 60s v1.30.2
# Cluster info
kubectl cluster-info --context kind-kind
# Kubernetes control plane is running at https://127.0.0.1:PORT
# CoreDNS is running at https://127.0.0.1:PORT/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
# Delete when done
kind delete cluster
kind delete cluster --name my-project
Kubeconfig
# Kind automatically merges into ~/.kube/config with context 'kind-<name>'
kubectl config get-contexts
# CURRENT NAME CLUSTER AUTHINFO NAMESPACE
# * kind-kind kind-kind kind-kind
# kind-my-project kind-my-project kind-my-project
# Switch between Kind clusters
kubectl config use-context kind-my-project
# Export kubeconfig for a specific cluster to a file
kind get kubeconfig --name my-project > /tmp/my-project-kubeconfig.yaml
export KUBECONFIG=/tmp/my-project-kubeconfig.yaml
# Print all clusters managed by Kind
kind get clusters
# kind
# my-project
Cluster Config File
Kind's power comes from its config file. You can define node topology, port mappings, extra volume mounts, feature gates, and kubeadm patches — all in a single YAML file committed to your repo.
Multi-Node Config
# kind-config.yaml — 1 control-plane + 2 workers
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
name: dev-cluster
nodes:
- role: control-plane
# Pin a specific Kubernetes version for this node
image: kindest/node:v1.30.2@sha256:XXXX
- role: worker
image: kindest/node:v1.30.2@sha256:XXXX
# Optional: add labels to the worker node
kubeadmConfigPatches:
- |
kind: JoinConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "tier=backend"
- role: worker
image: kindest/node:v1.30.2@sha256:XXXX
kubeadmConfigPatches:
- |
kind: JoinConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "tier=frontend"
# Create cluster from config file
kind create cluster --config kind-config.yaml
# Verify all nodes are up
kubectl get nodes -o wide
# NAME STATUS ROLES AGE VERSION
# dev-cluster-control-plane Ready control-plane 60s v1.30.2
# dev-cluster-worker Ready worker 45s v1.30.2
# dev-cluster-worker2 Ready worker 45s v1.30.2
Port Mappings
# Expose container ports to your host for ingress controllers
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
# Install nginx ingress controller on the control-plane node
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80 # maps localhost:80 → cluster port 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- containerPort: 30000 # NodePort range
hostPort: 30000
protocol: TCP
- role: worker
- role: worker
# After applying ingress controller to this cluster:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
# Wait for ingress controller
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90s
# Now localhost:80 reaches the ingress controller
curl http://localhost/
Extra Mounts
# Mount host directories into Kind nodes (for local file sharing)
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
nodes:
- role: control-plane
extraMounts:
# Mount local config files into the node's filesystem
- hostPath: /home/user/certs
containerPath: /certs
readOnly: true
# Mount a local source directory for development
- hostPath: /home/user/myapp
containerPath: /app
readOnly: false
propagation: None # None | HostToContainer | Bidirectional
- role: worker
extraMounts:
- hostPath: /home/user/data
containerPath: /data
readOnly: false
Local Registry
Kind clusters cannot pull images from your host Docker cache by default — they have their own containerd daemon. You need to either kind load docker-image or run a local registry that Kind nodes can reach.
# ── APPROACH 1: kind load docker-image (simplest) ──────────────
# Build image with your host Docker
docker build -t myapp:dev .
# Load into all nodes of the 'kind' cluster
kind load docker-image myapp:dev
# Image: "myapp:dev" with ID "sha256:..." not yet present on node...
# Loading image: myapp:dev
# Load into a specific named cluster
kind load docker-image myapp:dev --name dev-cluster
# Verify the image is available in the cluster
docker exec -it kind-control-plane crictl images | grep myapp
# docker.io/library/myapp dev sha256:... 100MB
# Use in a Pod (no imagePullPolicy: Never needed — image is pre-loaded)
kubectl run myapp --image=myapp:dev --image-pull-policy=Never
# ── APPROACH 2: Local registry container ───────────────────────
# Start a local registry on port 5001
docker run -d --restart=always -p 127.0.0.1:5001:5000 --name kind-registry registry:2
# Create a Kind cluster connected to the registry
# (See Part 2 for the full kind-config.yaml with containerdConfigPatches)
cat <<EOF | kind create cluster --config=-
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors]
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5001"]
endpoint = ["http://kind-registry:5000"]
EOF
# Connect the registry container to the Kind network
docker network connect kind kind-registry
# Now build, push to localhost:5001, and use in cluster
docker build -t localhost:5001/myapp:dev .
docker push localhost:5001/myapp:dev
kubectl run myapp --image=localhost:5001/myapp:dev
configmap/local-registry-hosting annotation so tooling like Skaffold can auto-detect the registry address.
Exercises
kubectl get nodes and kubectl cluster-info. Create a Deployment of nginx, expose it as a NodePort, and access it via port-forwarding (kubectl port-forward svc/nginx 8080:80). Delete the cluster when done.
kind-config.yaml with 1 control-plane and 2 workers. Add extraPortMappings for ports 80 and 443. Create the cluster from the config, install the nginx ingress controller, and deploy a test service accessible at localhost/test.
hello-kind:v1. Load it into your Kind cluster with kind load docker-image. Deploy it and verify the pod runs. Then tag a new build as hello-kind:v2, load it, and perform a rolling update.
Key Takeaways
- Kind runs each Kubernetes node as a Docker container — no VM, no hypervisor, starts in under 40 seconds
kind create clusterandkind delete clusterare all you need for the basics;--configunlocks full topology control- The config file (
kind-config.yaml) supports multi-node topologies,extraPortMappingsfor ingress,extraMountsfor host directories, andkubeadmConfigPatchesfor kubelet args - Use
kind load docker-imagefor ad-hoc local images, or wire a local registry for a workflow closer to production - Kind's context is always prefixed with
kind-: usekubectl config use-context kind-<name>to switch - In Part 2, we add GitHub Actions integration and show how a full E2E CI workflow runs in ~2 minutes
Next in This Track
In Part 2: Multi-Node & CI Usage, we build a full GitHub Actions workflow that spins up a Kind cluster, deploys your application, runs end-to-end tests, and tears down — all in about 2 minutes.