A tool for running local Kubernetes clusters using Docker containers as nodes, designed for development, testing, and CI/CD workflows.

Table of Contents#

  1. Overview
  2. Installation
  3. Cluster Management
  4. Multi-Node Clusters
  5. Ingress Configuration
  6. LoadBalancer Services
  7. Persistent Volumes
  8. Advanced Networking
  9. Working with Images
  10. CI/CD Integration
  11. Troubleshooting
  12. See Also
  13. 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: kubectl for 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/kind

macOS (Homebrew):

brew install kind

Go install:

go install sigs.k8s.io/kind@v0.27.0

2.3 Verify Installation#

kind --version

3. 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.4

3.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 --all

3.3 Create from Configuration File#

kind create cluster --name <cluster-name> --config cluster-config.yaml

4. Multi-Node Clusters#

4.1 Basic Multi-Node Configuration#

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

4.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: worker

4.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: backend

5. 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: TCP

Then deploy the NGINX Ingress controller:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

Wait for readiness:

kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \
  --selector=app.kubernetes.io/component=controller \
  --timeout=90s

5.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: 80

Access 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=90s

Determine the Docker network subnet:

docker network inspect -f '{{.IPAM.Config}}' kind

Configure 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-system

6.2 Cloud Provider Kind#

An alternative that simulates cloud load balancers:

go install sigs.k8s.io/cloud-provider-kind@latest
cloud-provider-kind

7. 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: standard

7.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: HostToContainer

Then 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: manual

7.3 Multiple Volume Mounts#

nodes:
- role: worker
  extraMounts:
  - hostPath: /data/postgres
    containerPath: /mnt/postgres
  - hostPath: /data/redis
    containerPath: /mnt/redis

8. 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: true

Then install your CNI after cluster creation:

# Example: Calico
kubectl apply -f https://raw.githubusercontent.com/projectcalico/calico/v3.29.2/manifests/calico.yaml

8.3 IPv6 and Dual-Stack#

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  ipFamily: dual

8.4 API Server Address and Port#

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  apiServerAddress: "127.0.0.1"
  apiServerPort: 6443

8.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-registry

Configure 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 here

10.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 tests

Troubleshooting#

IssueCauseSolution
ERROR: failed to create clusterDocker not running or insufficient resourcesStart Docker; ensure at least 4 GB RAM and 20 GB disk available
Nodes stuck in NotReadyCNI not deployed (when disableDefaultCNI: true)Install your chosen CNI plugin after cluster creation
Image pull ErrImageNeverPullImage not loaded into Kind's containerdRun kind load docker-image <image> before deploying
Port mapping conflictHost port already in useChange hostPort in the cluster config or stop the conflicting process
too many open filesSystem inotify limits too lowIncrease fs.inotify.max_user_watches and fs.inotify.max_user_instances via sysctl
Ingress not reachable on localhostMissing extraPortMappings or ingress-ready labelAdd port mappings (80, 443) and the node label in cluster config
PVC stuck in PendingNo StorageClass or provisioner not runningVerify the standard StorageClass exists: kubectl get sc
DNS resolution failures inside podsCoreDNS not ready or Docker DNS config issuesWait for CoreDNS pods; check Docker daemon DNS settings
Cluster creation slowLarge image pullPre-pull the kindest/node image: docker pull kindest/node:v1.31.4
Multiple clusters, wrong contextkubectl pointing to a different clusterSwitch context: kubectl config use-context kind-<cluster-name>

See Also#

Sources#