Back to Distributed Systems & Kubernetes Series

Kind Track Part 1: Setup & Config

June 6, 2026 Wasil Zafar 30 min read

Kind (Kubernetes IN Docker) creates Kubernetes clusters as Docker containers. It starts in seconds, supports multi-node topologies, and is the tool of choice for CI pipelines and local controller development — because it uses your existing Docker daemon with no VM overhead.

Table of Contents

  1. Kind vs Minikube
  2. Installation
  3. Your First Cluster
  4. Cluster Config File
  5. Local Registry
  6. Exercises
  7. Key Takeaways

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.

When to use Minikube vs Kind: Use Minikube when you need a persistent local cluster for day-to-day development, addons (ingress, dashboard), or you prefer a GUI-driven workflow. Use Kind when you're writing controllers/operators, need CI integration, want faster cluster creation, or are testing against multiple Kubernetes versions in parallel.

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
Official script: The Kind project provides a local registry setup script that handles network configuration and adds the configmap/local-registry-hosting annotation so tooling like Skaffold can auto-detect the registry address.

Exercises

Exercise 1 — First Cluster: Install Kind and create a default cluster. Run 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.
Exercise 2 — Config File: Write a 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.
Exercise 3 — Local Image: Write a minimal Go HTTP server that returns "Hello from Kind". Build it as a Docker image tagged 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

Key Takeaways:
  • Kind runs each Kubernetes node as a Docker container — no VM, no hypervisor, starts in under 40 seconds
  • kind create cluster and kind delete cluster are all you need for the basics; --config unlocks full topology control
  • The config file (kind-config.yaml) supports multi-node topologies, extraPortMappings for ingress, extraMounts for host directories, and kubeadmConfigPatches for kubelet args
  • Use kind load docker-image for ad-hoc local images, or wire a local registry for a workflow closer to production
  • Kind's context is always prefixed with kind-: use kubectl 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.