Published on

Running a kubernetes cluster locally with kind

Previously I showed how to run kubernetes locally with kubeadm and VMs but sometimes that is overkill so I wanted to show how to run kind which is "kuberetes in docker".

Creating your first cluster

kind is a very flexible way to run kubernetes locally and allows you to run single node or multinode clusters while having the flexibility to use all the features of kubernetes success as ingress.

To create your first cluster it is as simple as running:

 kind create cluster  
Creating cluster "kind" ...
  Ensuring node image (kindest/node:v1.27.3) 🖼 
  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
Have a question, bug, or feature request? Let us know! 🙂

You now have a functioning kubernetes cluster and you can view what it created:

 k get node
NAME                 STATUS   ROLES           AGE     VERSION
kind-control-plane   Ready    control-plane   4m26s   v1.27.3

You can also verify that it is running inside docker:

 docker ps
CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS         PORTS                       NAMES
1c3ba74dc29b   kindest/node:v1.27.3   "/usr/local/bin/entr…"   3 minutes ago   Up 3 minutes>6443/tcp   kind-control-plane

Making the cluster useful

There are a few things you'll notice with the command we ran originally:

  • It grabbed the latest kubernetes version available
  • It is running a single node cluster
  • No ingress available

Luckily kind makes it really easy to customize your local cluster to be what you want it to be by using a YAML configuration.

Create the configuration:

kind: Cluster
- role: control-plane
  - |
    kind: InitConfiguration
        node-labels: "ingress-ready=true"
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
- role: worker
- role: worker
- role: worker

With this we've now generated a 4 node cluster where we have a single control-plane and three workers. Then we defined some extra configuration on the control-plane:

  • kubeadmConfigPatches: We want to change the default configuration the cluster uses so it'll tag the nodes with the ingress-ready label so the controller will use them.
  • extraPortMappings: allow the local host to make requests to the Ingress controller over ports 80/443
  • node-labels: only allow the ingress controller to run on specific node(s) matching the label selector

So now we can create the new cluster with the configuration. Save that config as kind_config.yml and then run:

 kind create cluster --image kindest/node:v1.25.11 --config kind_config.yml --name kind-multinode

This time I've added a few additional flags on the commandline. --image allows us to use a different version of kubernetes and --name allows us to make more than one cluster. So if you didn't destroy the first cluster you'll see we have two of them now:

 kind get clusters

but kind will swap the to the newest cluster by default:

 kubectl config current-context
 kubectl get node
NAME                           STATUS   ROLES           AGE    VERSION
kind-multinode-control-plane   Ready    control-plane   107s   v1.25.11
kind-multinode-worker          Ready    <none>          88s    v1.25.11
kind-multinode-worker2         Ready    <none>          88s    v1.25.11
kind-multinode-worker3         Ready    <none>          88s    v1.25.11

Now we need to get the ingress-nginx controller installed so we can start using our cluster with ingress:

 kubectl apply -f

The manifests contains kind specific patches to forward the hostPorts to the ingress controller, set taint tolerations and schedule it to the custom labelled node.

This will take a little bit of time to get up and running, you can monitor it by running:

kubectl wait --namespace ingress-nginx \
  --for=condition=ready pod \ \

or just manually check the status:

 kubectl get all -n ingress-nginx
NAME                                            READY   STATUS              RESTARTS   AGE
pod/ingress-nginx-admission-create-bbmlc        0/1     Completed           0          68s
pod/ingress-nginx-admission-patch-qlnr8         0/1     Completed           2          68s
pod/ingress-nginx-controller-5f748f78c8-6tc6b   0/1     ContainerCreating   0          68s
NAME                                         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                      AGE
service/ingress-nginx-controller             NodePort   <none>        80:31771/TCP,443:31759/TCP   68s
service/ingress-nginx-controller-admission   ClusterIP   <none>        443/TCP                      68s
NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   0/1     1            0           68s
NAME                                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-nginx-controller-5f748f78c8   1         1         0       68s
NAME                                       COMPLETIONS   DURATION   AGE
job.batch/ingress-nginx-admission-create   1/1           22s        68s
job.batch/ingress-nginx-admission-patch    1/1           35s        68s

Once ingress-nginx-controller is in Running state you are read to go!

Deploying your first app

To prove that the cluster is working correctly we will deploy httpbin which is a nice little API server so we can prove everything is working.

Create a httbin.yml file and paste this into it:

apiVersion: v1
kind: Service
  name: httpbin
    app: httpbin
    service: httpbin
  - name: http
    port: 8000
    targetPort: 8080
    app: httpbin
apiVersion: apps/v1
kind: Deployment
  name: httpbin
  replicas: 2
      app: httpbin
      version: v1
        app: httpbin
        version: v1
      - image:
        imagePullPolicy: IfNotPresent
        name: httpbin
        - containerPort: 8080

This is creating a couple of Kubernetes resources:

  • Service: This is exposing the port to the ingress
  • Deployment: This is actually launching the service

So we are not using the ingress yet but we can prove that we can launch the service at least. So apply those manifests:

 kubectl apply -f httpbin.yml 
service/httpbin created
deployment.apps/httpbin created

You should see two pods come up. You should wait for them to get into the Running status:

 kubectl get pod -o wide
NAME                      READY   STATUS    RESTARTS   AGE   IP           NODE                     NOMINATED NODE   READINESS GATES
httpbin-5c5494967-2z5wz   1/1     Running   0          48s   kind-multinode-worker3   <none>           <none>
httpbin-5c5494967-9lf47   1/1     Running   0          72s   kind-multinode-worker    <none>           <none>

We can now use port forwarding to access it. httpbin is exposed on 8000 so lets create port 9000 on our host that forwards to it:

 kubectl port-forward service/httpbin 9000:8000
Forwarding from -> 80
Forwarding from [::1]:9000 -> 80

You can access it via:

 curl localhost:9000/get 
  "args": {},
  "headers": {
    "Accept": [

Using Ingress

Now to use the ingress rather than port forwarding we create one additional resource, the Ingress:

kind: Ingress
  name: httpbin-ingress
  annotations: /$2
  ingressClassName: nginx
  - http:
      - path: /httpbin(/|$)(.*)
        pathType: ImplementationSpecific
            name: httpbin
              number: 8000

There are a few critical options here. The first is the annotation to rewrite the path so it doesn't include /httpbin/ when it sends the request to the service and then the path and pathType so it knows which paths to send to which service.

Now you should be able to hit your local host and get routed to your kubernetes service:

 curl localhost/httpbin/get

Success! Now you have a multinode kubernetes cluster that has an ingress controller!

Next Steps

The cluster can be used like a production cluster now for local development! You could setup Grafana, ArgoCD, etc. to run inside the cluster.