Installing Kubernetes with kind

By Vishvesh Patel · DevOps EngineerUpdated September 12, 2026

Run multi-node Kubernetes in Docker with kind. Cluster config, loading local images, port mappings for Ingress, pinning versions, and using it in CI.

kind, Kubernetes IN Docker, runs each cluster node as a Docker container. Clusters start in well under a minute, multi-node is a few lines of config, and deleting one is instant.

That speed is why it has become the default for CI pipelines and for anyone who recreates clusters often.

Install

# macOS
brew install kind

# Linux
curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind

# Windows
winget install Kubernetes.kind

It needs a working Docker or Podman installation, and nothing else.

A first cluster

kind create cluster --name dev
Creating cluster "dev" ...
 ✓ Ensuring node image (kindest/node:v1.31.2)
 ✓ Preparing nodes
 ✓ Starting control-plane
 ✓ Installing CNI
 ✓ Installing StorageClass
Set kubectl context to "kind-dev"
kubectl get nodes
NAME                STATUS   ROLES           AGE   VERSION
dev-control-plane   Ready    control-plane   32s   v1.31.2

Note the kubectl context is kind-dev. Kind prefixes every context name with kind-.

kind get clusters
kind delete cluster --name dev

Multi-node, which is the point

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
  - role: control-plane
  - role: worker
  - role: worker
kind create cluster --name dev --config kind-config.yaml

Create a multi-node cluster even for simple work. Single-node clusters hide everything interesting about scheduling: pod anti-affinity does nothing, topology spread constraints do nothing, ReadWriteOnce volume conflicts never appear, and node drains are meaningless. Those are exactly the behaviours that surprise people in a real cluster.

Pinning the Kubernetes version

nodes:
  - role: control-plane
    image: kindest/node:v1.30.6
  - role: worker
    image: kindest/node:v1.30.6

Pin it to match the cluster you actually deploy to. Particularly useful for testing an upgrade before it happens.

Loading local images

This is the one piece of friction with kind. Each node is a Docker container with its own containerd image store, so your host's images are invisible to it. A Pod referencing a locally built image gets ImagePullBackOff.

docker build -t myapp:dev .
kind load docker-image myapp:dev --name dev

Set imagePullPolicy: IfNotPresent, since Always, the default when the tag is latest or absent, makes the kubelet attempt a registry pull that will fail.

kind load image-archive myapp.tar --name dev     # from a saved tarball

Forgetting kind load is the single most common kind problem, and it is worth adding to any build script that targets kind.

Ingress

Reaching an Ingress from your host needs port mappings declared at cluster creation. They cannot be added afterwards:

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
kind create cluster --config kind-config.yaml

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

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

localhost then reaches your Ingress. The ingress-ready=true label is what the kind-specific ingress-nginx manifest uses to place the controller on the node with the port mappings.

LoadBalancer Services stay <pending> on kind unless you install MetalLB. For local work, Ingress or kubectl port-forward is simpler.

Mounting host directories

Useful for iterating on code without rebuilding images:

nodes:
  - role: control-plane
    extraMounts:
      - hostPath: /Users/me/work/payments
        containerPath: /src

The path is then available to a hostPath volume inside the cluster.

kind in CI

kind is the standard way to run integration tests against a real Kubernetes API:

# .github/workflows/test.yml
name: test
on: [push]
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: helm/kind-action@v1
        with:
          cluster_name: ci
          config: kind-config.yaml
      - run: docker build -t myapp:ci .
      - run: kind load docker-image myapp:ci --name ci
      - run: kubectl apply -f manifests/
      - run: kubectl wait --for=condition=available deployment/myapp --timeout=120s
      - run: ./run-integration-tests.sh

Cluster creation adds well under a minute, which is what makes this practical on every push. Testing against a real API server catches admission, RBAC and manifest errors that no amount of YAML linting will.

Debugging

kubectl get pods -A                          # control plane should all be Running
docker ps                                    # each node is a container
docker exec -it dev-control-plane bash        # shell onto a node
kind export logs ./kind-logs --name dev      # everything, for inspection

If a cluster will not create at all, it is usually Docker: not running, out of disk, or out of memory. docker system prune and raising Docker Desktop's memory allocation resolve most cases.

kind or minikube?

kindminikube
StartupVery fastSlower
Multi-nodeTrivialSupported
Add-onsInstall them yourselfOne command each
Local imageskind load docker-imageeval $(minikube docker-env)
LoadBalancerNeeds MetalLBminikube tunnel
CIDesigned for itWorkable

kind for speed, multi-node and CI. minikube if you want Ingress and metrics-server without assembling them.

Quick reference

TaskCommand
Createkind create cluster --name dev
From configkind create cluster --config kind-config.yaml
Listkind get clusters
Load an imagekind load docker-image <image> --name dev
Kubeconfigkind get kubeconfig --name dev
Shell onto a nodedocker exec -it dev-control-plane bash
Export logskind export logs ./logs --name dev
Deletekind delete cluster --name dev

Key takeaways

  • Each node is a Docker container, which is why kind starts and deletes so quickly.
  • kind load docker-image is mandatory for local images: the cluster cannot see your host's Docker store.
  • Set imagePullPolicy: IfNotPresent, or the kubelet tries a registry and fails.
  • Create multi-node clusters. Single-node hides affinity, spread and drain behaviour entirely.
  • extraPortMappings must be set at creation time and are what make Ingress reachable on localhost.
  • LoadBalancer stays <pending> without MetalLB: use Ingress or port-forward instead.
  • kind in CI catches admission, RBAC and manifest errors that linting cannot.

Check your understanding

Exercise

Ingress unreachable from the host

An ingress-nginx controller is running in a kind cluster, but curl localhost times out. The cluster was created with kind create cluster with no config file. What is wrong?

Exercise

Anti-affinity that does nothing

A Deployment with podAntiAffinity requiring replicas on different nodes runs three replicas happily on a single-node kind cluster. Why does the rule appear ignored?


Frequently Asked Questions

What is kind and how is it different from minikube?

kind runs each Kubernetes node as a Docker container rather than in a VM, which makes cluster creation and deletion very fast and multi-node configurations trivial. That speed is why it is the usual choice for CI. minikube is somewhat slower but ships curated add-ons, so Ingress and metrics-server are one command each rather than a manual install. The practical split is kind for speed, multi-node and pipelines; minikube for a batteries-included local environment.

Why does my locally built image give ImagePullBackOff in kind?

Because kind nodes are Docker containers with their own containerd image store, and your host's Docker images are not visible inside them. Run kind load docker-image myapp:dev --name <cluster> to copy it in. Also set imagePullPolicy: IfNotPresent, since Always, which is the default when the tag is latest or omitted, makes the kubelet try a registry pull that fails. Forgetting kind load is the most common kind problem by a wide margin.

How do I create a multi-node cluster with kind?

Write a config file listing the nodes you want, with one control-plane role and one or more worker roles, then pass it with kind create cluster --config kind-config.yaml. It is worth doing even for simple work: on a single node, pod anti-affinity and topology spread constraints have no observable effect, ReadWriteOnce volume conflicts never occur, and node drains are meaningless. All of which then surprise you in a real cluster.

How do I use an Ingress controller with kind?

Declare extraPortMappings for ports 80 and 443 in the cluster config, and label the control-plane node with ingress-ready=true. These must be set when the cluster is created, because Docker fixes published ports at container creation. Adding them later means recreating the cluster. Then apply the kind-specific ingress-nginx manifest, which schedules the controller onto the labelled node, and localhost will reach your Ingress.

Why is my LoadBalancer Service pending in kind?

There is no cloud provider to fulfil the request, so the external IP is never assigned. Install MetalLB if you genuinely need LoadBalancer Services to work, but for local development an Ingress with extraPortMappings, or simply kubectl port-forward, is considerably less setup for the same result.

Can I use kind in CI pipelines?

Yes, and it is the standard approach for integration testing against a real Kubernetes API. helm/kind-action sets up a cluster in a GitHub Actions job in under a minute, after which you build your image, kind load it, apply your manifests, and run tests. Because it is a genuine API server, it catches admission controller rejections, RBAC problems and invalid manifests that YAML linting cannot detect.

Continue Learning

Explore Related Topics

Try the Tool

Practise this in the browser

Kubernetes Playground

Practise kubectl against a simulated cluster in your browser.

When it breaks

Related Resources