A tool for running local Kubernetes clusters using Docker containers as nodes, designed for development, testing, and CI/CD workflows.
Table of Contents#
- Overview
- Installation
- Cluster Management
- Multi-Node Clusters
- Ingress Configuration
- LoadBalancer Services
- Persistent Volumes
- Advanced Networking
- Working with Images
- CI/CD Integration
- Troubleshooting
- See Also
- Sources
1. Overview#
Kind (Kubernetes in Docker) runs Kubernetes cluster nodes as Docker containers rather than virtual machines. Each container acts as a Kubernetes node running kubelet, containerd, and the necessary control plane or worker components. This provides a fast, lightweight, and disposable Kubernetes environment.
Key features:
- Fast startup - clusters ready in under 60 seconds
- Multi-node - simulate production topologies with multiple control plane and worker nodes
- Conformant - runs real Kubernetes binaries, passes conformance tests
- CI/CD friendly - no VM overhead, works in containers-in-containers environments
- Portable - works on Linux, macOS, and Windows (anywhere Docker runs)
- Multiple clusters - run several named clusters simultaneously
2. Installation#
2.1 Prerequisites#
- Docker installed and running
- Optional:
kubectlfor cluster interaction
2.2 Install Kind#
Binary download:
curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.27.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kindmacOS (Homebrew):
brew install kindGo install:
go install sigs.k8s.io/kind@v0.27.02.3 Verify Installation#
kind --version3. Cluster Management#
3.1 Create a Cluster#
# Default single-node cluster named "kind"
kind create cluster
# Named cluster with specific Kubernetes version
kind create cluster --name <cluster-name> --image kindest/node:v1.31.43.2 Manage Clusters#
# List clusters
kind get clusters
# Get kubeconfig
kind get kubeconfig --name <cluster-name>
# Export kubeconfig to a file
kind get kubeconfig --name <cluster-name> > kubeconfig.yaml
# Set kubectl context
kubectl cluster-info --context kind-<cluster-name>
# Delete a cluster
kind delete cluster --name <cluster-name>
# Delete all clusters
kind delete clusters --all3.3 Create from Configuration File#
kind create cluster --name <cluster-name> --config cluster-config.yaml4. Multi-Node Clusters#
4.1 Basic Multi-Node Configuration#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker4.2 High Availability Control Plane#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: control-plane
- role: control-plane
- role: worker
- role: worker
- role: worker4.3 Node Labels and Port Mappings#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCP
- role: worker
labels:
tier: frontend
- role: worker
labels:
tier: backend5. Ingress Configuration#
5.1 NGINX Ingress Controller#
First, create a cluster with port mappings and the ingress-ready label:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs:
node-labels: "ingress-ready=true"
extraPortMappings:
- containerPort: 80
hostPort: 80
protocol: TCP
- containerPort: 443
hostPort: 443
protocol: TCPThen deploy the NGINX Ingress controller:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yamlWait for readiness:
kubectl wait --namespace ingress-nginx \
--for=condition=ready pod \
--selector=app.kubernetes.io/component=controller \
--timeout=90s5.2 Test Ingress#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: test-ingress
spec:
rules:
- host: <test-hostname>
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: <service-name>
port:
number: 80Access via http://<test-hostname> after adding an entry to /etc/hosts pointing to 127.0.0.1.
6. LoadBalancer Services#
Kind does not natively support LoadBalancer service types. Use one of these approaches:
6.1 MetalLB#
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.14.9/config/manifests/metallb-native.yaml
kubectl wait --namespace metallb-system \
--for=condition=ready pod \
--selector=app=metallb \
--timeout=90sDetermine the Docker network subnet:
docker network inspect -f '{{.IPAM.Config}}' kindConfigure an IP address pool within that subnet:
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: kind-pool
namespace: metallb-system
spec:
addresses:
- <start-ip>-<end-ip>
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: kind-l2
namespace: metallb-system6.2 Cloud Provider Kind#
An alternative that simulates cloud load balancers:
go install sigs.k8s.io/cloud-provider-kind@latest
cloud-provider-kind7. Persistent Volumes#
7.1 Default Local Storage#
Kind includes a default StorageClass (standard) backed by the rancher.io/local-path provisioner. PVCs are automatically bound:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: <pvc-name>
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: <size>
storageClassName: standard7.2 Host Path Mounts#
Mount host directories into Kind nodes for persistent data that survives cluster recreation:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
- hostPath: /tmp/kind-data
containerPath: /data
readOnly: false
propagation: HostToContainerThen create a PV backed by that path:
apiVersion: v1
kind: PersistentVolume
metadata:
name: host-pv
spec:
capacity:
storage: <size>
accessModes:
- ReadWriteOnce
hostPath:
path: /data
type: DirectoryOrCreate
storageClassName: manual
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: host-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: <size>
storageClassName: manual7.3 Multiple Volume Mounts#
nodes:
- role: worker
extraMounts:
- hostPath: /data/postgres
containerPath: /mnt/postgres
- hostPath: /data/redis
containerPath: /mnt/redis8. Advanced Networking#
8.1 Custom Pod and Service Subnets#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/12"8.2 Disable Default CNI#
To install a custom CNI (Calico, Cilium, etc.):
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
disableDefaultCNI: trueThen install your CNI after cluster creation:
# Example: Calico
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/calico.yaml8.3 IPv6 and Dual-Stack#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
ipFamily: dual8.4 API Server Address and Port#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "127.0.0.1"
apiServerPort: 64438.5 Proxy Settings#
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
kubeProxyMode: "ipvs"9. Working with Images#
9.1 Load Local Images#
Kind clusters use their own containerd runtime, so locally-built images must be loaded explicitly:
# Build locally
docker build -t <image-name>:<tag> .
# Load into the Kind cluster
kind load docker-image <image-name>:<tag> --name <cluster-name>9.2 Load from Archive#
docker save <image-name>:<tag> -o image.tar
kind load image-archive image.tar --name <cluster-name>9.3 Use Local Registry#
Set up a local Docker registry and connect it to the Kind network:
# Create registry container
docker run -d --restart=always -p 5001:5000 --name kind-registry registry:2
# Connect to kind network
docker network connect kind kind-registryConfigure Kind to use it:
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
containerdConfigPatches:
- |-
[plugins."io.containerd.grpc.v1.cri".registry.mirrors."localhost:5001"]
endpoint = ["http://kind-registry:5000"]Push images to localhost:5001/<image>:<tag> and reference them in manifests.
10. CI/CD Integration#
10.1 GitHub Actions#
steps:
- uses: actions/checkout@v4
- uses: engineerd/setup-kind@v0.6.2
with:
version: "v0.27.0"
- name: Test
run: |
kubectl cluster-info
kubectl get nodes
# Run your tests here10.2 GitLab CI#
test:
image: docker:latest
services:
- docker:dind
before_script:
- apk add --no-cache curl
- curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.27.0/kind-linux-amd64
- chmod +x ./kind && mv ./kind /usr/local/bin/
- kind create cluster
- curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl && mv kubectl /usr/local/bin/
script:
- kubectl get nodes
- # Run your testsTroubleshooting#
| Issue | Cause | Solution |
|---|---|---|
ERROR: failed to create cluster | Docker not running or insufficient resources | Start Docker; ensure at least 4 GB RAM and 20 GB disk available |
Nodes stuck in NotReady | CNI not deployed (when disableDefaultCNI: true) | Install your chosen CNI plugin after cluster creation |
Image pull ErrImageNeverPull | Image not loaded into Kind's containerd | Run kind load docker-image <image> before deploying |
| Port mapping conflict | Host port already in use | Change hostPort in the cluster config or stop the conflicting process |
too many open files | System inotify limits too low | Increase fs.inotify.max_user_watches and fs.inotify.max_user_instances via sysctl |
| Ingress not reachable on localhost | Missing extraPortMappings or ingress-ready label | Add port mappings (80, 443) and the node label in cluster config |
PVC stuck in Pending | No StorageClass or provisioner not running | Verify the standard StorageClass exists: kubectl get sc |
| DNS resolution failures inside pods | CoreDNS not ready or Docker DNS config issues | Wait for CoreDNS pods; check Docker daemon DNS settings |
| Cluster creation slow | Large image pull | Pre-pull the kindest/node image: docker pull kindest/node:v1.31.4 |
| Multiple clusters, wrong context | kubectl pointing to a different cluster | Switch context: kubectl config use-context kind-<cluster-name> |