Skip to content
Commitpedia
Log in Join & edit

Learn Kubernetes on your laptop with kind: from first cluster to Ingress

Build a throwaway Kubernetes cluster in Docker with kind, deploy a web app with config, secrets and probes, scale it, roll out and roll back, publish it through Ingress, then delete it all.

Not checked by a trusted editor yet Last edited Outdated?

Edit history of Learn Kubernetes on your laptop with kind: from first cluster to Ingress: 1 recorded edit
Edit history · 1 edit
Type
Article
Edits
1
Editors
1
First issued
15 Oct 2026
Last edit
15 Oct 2026
Reading
11 min

This tutorial gives you a complete Kubernetes workout on your own computer. You use kind (Kubernetes in Docker), which runs each Kubernetes node as a container, so a whole cluster starts in a minute or two and disappears with one command. Nothing you do here touches a real server.

It is written for people who know the basic idea of containers and want to see how the main Kubernetes objects fit together in practice. Every step says why you do it, what to type and what you should see.

What you will build

  • A single-node Kubernetes cluster running inside Docker on your laptop.
  • A small nginx website, defined as a Deployment with two replicas and a Service in front of it.
  • Page content stored in a ConfigMap and a token stored in a Secret.
  • Readiness and liveness probes, plus CPU and memory requests and a memory limit.
  • A rolling update to a newer image, a deliberately broken update and a rollback.
  • An Ingress that publishes the site outside the cluster, using Cloud Provider KIND as the kind documentation describes.

Before you start

  • A computer with Linux, macOS or Windows and about 4 GB of free memory for Docker.
  • A container runtime: Docker, Podman or nerdctl. kind detects which one is installed. The commands here assume Docker (on macOS and Windows, Docker Desktop must be running).
  • Permission to run one helper program with sudo (macOS, WSL2) or as administrator (Windows) for the Ingress step.
  • Internet access to download kind, kubectl and container images.
  • Time: about 60 to 90 minutes, most of it reading and checking output.
  • Knowledge assumed: using a terminal, what a container image is, and how to read a short YAML file. Helpful background concepts: Pods, ReplicaSets and Deployments; Services and Ingress; ConfigMaps and Secrets.

Step 1: Install kind, kubectl and Cloud Provider KIND

You need three programs. kind creates and deletes clusters. kubectl is the standard Kubernetes client that talks to any cluster. cloud-provider-kind runs on your computer next to the cluster and gives it load balancer and Ingress support, which a laptop cluster does not have by itself.

On macOS with Homebrew:

brew install kind
brew install kubectl
brew install cloud-provider-kind

The Homebrew package for cloud-provider-kind is maintained by the community. You can also install it with Go, which places the binary in ~/go/bin:

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
go install sigs.k8s.io/cloud-provider-kind@latest
sudo install ~/go/bin/cloud-provider-kind /usr/local/bin

On Linux, download the kind release binary as the kind quick start shows (it names the current release; the version below is the one listed when this was written), and install kubectl from the Kubernetes documentation:

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
[ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.33.0/kind-linux-amd64
[ $(uname -m) = aarch64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.33.0/kind-linux-arm64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

On Windows, kind is available with choco install kind. Check that everything answers:

kind version
kubectl version --client
docker info --format '{{.ServerVersion}}'

Each command should print a version. If docker info fails, start Docker first. Keep kubectl within one minor version of the cluster’s Kubernetes version; a fresh install of both usually satisfies that.

Step 2: Create the cluster

kind create cluster pulls a node image, starts it as a container and configures kubectl to use it. The default cluster is called kind and its kubectl context is kind-kind.

kind create cluster
kind get clusters
kubectl cluster-info --context kind-kind
kubectl get nodes

kind get clusters should list kind. kubectl get nodes shows one node, kind-control-plane. It may say NotReady for a few seconds while networking starts; run the command again until it says Ready.

Step 3: Make a namespace to work in

A namespace keeps your objects together and makes cleanup easy. Setting it as the default for the current context means you do not have to type -n demo on every command.

kubectl create namespace demo
kubectl config set-context --current --namespace=demo
kubectl config view --minify | grep namespace

The last command should print namespace: demo.

Step 4: Store configuration in a ConfigMap and a Secret

Configuration belongs outside the container image, so the same image can run anywhere. A ConfigMap holds ordinary settings or files; a Secret holds sensitive values. Create a web page and put it into a ConfigMap, then create a Secret with a placeholder token.

cat > index.html <<'EOF'
<h1>Hello from kind</h1>
<p>Served by nginx from a ConfigMap.</p>
EOF
kubectl create configmap web-html --from-file=index.html
kubectl create secret generic web-secret --from-literal=API_TOKEN=CHANGE_ME
kubectl get configmap,secret

You should see web-html with 1 data item and web-secret of type Opaque. Secret values are only base64-encoded, not encrypted, so anyone allowed to read Secrets in the namespace can read them. Never commit real tokens to Git.

Step 5: Deploy the app with a Deployment and a Service

A Deployment keeps a set number of identical Pods running and replaces them in a controlled way when you change it. A Service gives those Pods one stable name and address inside the cluster. Save the following as app.yaml. It mounts the ConfigMap as the nginx web root, passes the Secret in as an environment variable, adds probes and sets resource requests.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - name: http
              containerPort: 80
          env:
            - name: API_TOKEN
              valueFrom:
                secretKeyRef:
                  name: web-secret
                  key: API_TOKEN
          volumeMounts:
            - name: html
              mountPath: /usr/share/nginx/html
              readOnly: true
          readinessProbe:
            httpGet:
              path: /
              port: http
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi
      volumes:
        - name: html
          configMap:
            name: web-html
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: http

The readiness probe decides whether a Pod receives traffic; the liveness probe restarts the container if it stops answering. Requests tell the scheduler how much CPU and memory to reserve; the memory limit stops one Pod from using too much. Apply the file and wait for the rollout:

kubectl apply -f app.yaml
kubectl rollout status deployment/web
kubectl get deployment,pods,service -o wide

Expect deployment "web" successfully rolled out, two Pods with 1/1 ready and a Service of type ClusterIP. The Service is only reachable inside the cluster, so use a port forward to test it from your laptop. Run this in a second terminal and leave it running:

kubectl port-forward service/web 8080:80
curl http://localhost:8080/
kubectl exec deploy/web -- printenv API_TOKEN

curl should return your HTML and printenv should print CHANGE_ME. Stop the port forward with Ctrl+C when you are done.

Try changing the page: edit index.html, then replace the ConfigMap. Files mounted from a ConfigMap are updated in running Pods after a short delay, without a restart. Environment variables from ConfigMaps or Secrets are not; for those you restart the Pods with kubectl rollout restart deployment/web.

kubectl create configmap web-html --from-file=index.html --dry-run=client -o yaml | kubectl apply -f -

Step 6: Scale up and down

Scaling changes the number of replicas. The Deployment creates or removes Pods until the count matches, and the Service automatically includes only the ready ones.

kubectl scale deployment/web --replicas=4
kubectl get pods
kubectl get endpointslices -l kubernetes.io/service-name=web
kubectl scale deployment/web --replicas=2

After scaling to 4 you should see four Pods and four addresses in the EndpointSlice; after scaling back to 2, two Pods are terminated. In a real project you would also change replicas in app.yaml, because the next kubectl apply sets it back to the value in the file.

Step 7: Roll out an update, break it and roll back

When you change the Pod template, for example the image, the Deployment performs a rolling update: it starts new Pods and removes old ones gradually. By default at most 25% of the desired Pods may be unavailable and at most 25% extra may be created during the update. Record a reason for each change with the kubernetes.io/change-cause annotation so the history is readable.

kubectl set image deployment/web nginx=nginx:1.28
kubectl annotate deployment/web kubernetes.io/change-cause="nginx 1.28"
kubectl rollout status deployment/web
kubectl rollout history deployment/web

Now make a mistake on purpose by asking for an image tag that does not exist:

kubectl set image deployment/web nginx=nginx:does-not-exist
kubectl annotate deployment/web kubernetes.io/change-cause="broken tag"
kubectl get pods

One new Pod shows ErrImagePull or ImagePullBackOff. Because it never becomes ready, the rolling update stops there and the old Pods keep serving traffic. That is the readiness probe and the rollout strategy protecting you. Roll back to the previous revision:

kubectl rollout undo deployment/web
kubectl rollout status deployment/web
kubectl rollout history deployment/web
kubectl get deployment web -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'

The image is nginx:1.28 again. To go back further, pick a number from the history and use kubectl rollout undo deployment/web --to-revision=1.

Step 8: Publish the app with an Ingress

An Ingress routes HTTP requests from outside the cluster to Services, by host name and path. It needs an Ingress controller. Current kind documentation uses Cloud Provider KIND (version 0.9.0 or later), which supports Ingress natively, so you do not install a separate controller.

Open a new terminal and start it. Leave it running for the rest of the tutorial. It needs privileges to open ports and to talk to the container runtime: on macOS and WSL2 the project says to run it with sudo, and on Windows in a terminal opened with Run as administrator. On Linux, run it as a user that can use Docker, with sudo if it reports permission errors.

Check before you run this

  • Runs with administrator rights. Make sure you understand every line.
sudo cloud-provider-kind

On a one-node kind cluster your app runs on the control-plane node, which is labeled to be excluded from external load balancers. The Cloud Provider KIND project says to remove that label in this case:

kubectl label node kind-control-plane node.kubernetes.io/exclude-from-external-load-balancers-

Save this as ingress.yaml and apply it:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web
spec:
  rules:
    - http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web
                port:
                  number: 80
kubectl apply -f ingress.yaml
kubectl get ingress web

After a short while the ADDRESS column shows an IP address. On Linux you can call that address directly:

INGRESS_IP=$(kubectl get ingress web -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
curl "http://${INGRESS_IP}/"

On macOS and Windows, Docker runs inside a virtual machine, so that IP is not reachable from your computer. Cloud Provider KIND then starts a helper container whose name begins with kindccm- and publishes the port on localhost. Find the host port that maps to port 80 and use it:

docker ps --filter name=kindccm --format '{{.Names}}  {{.Ports}}'
curl http://localhost:HOST_PORT/

Replace HOST_PORT with the number shown before ->80/tcp. You should get your HTML page.

Step 9: Inspect logs and events

When something goes wrong in Kubernetes, three commands answer most questions: logs shows what the container printed, describe shows an object’s state and recent events, and get events lists what the cluster did and why.

kubectl logs deployment/web --tail=20
kubectl logs -l app=web --prefix --tail=5
kubectl describe deployment web
kubectl describe pod -l app=web
kubectl get events --sort-by=.lastTimestamp

In the logs you should see nginx access lines for your curl requests. In the events you can find the story of this tutorial: scheduled Pods, pulled images, the failed pull of nginx:does-not-exist and the scaling actions. Events are kept only for a limited time, so look soon after a problem.

Check that it works

Run this end-to-end check. Every line should succeed:

kubectl get deployment web
kubectl get pods -l app=web
kubectl get service web
kubectl get ingress web
kubectl exec deploy/web -- printenv API_TOKEN
  • The Deployment shows 2/2 ready and the image is nginx:1.28.
  • Both Pods are Running with 1/1 ready and few or no restarts.
  • The Ingress has an address, and curl through the Ingress (Step 8) returns your page.
  • printenv prints the value from the Secret.

If something goes wrong

  • kind create cluster fails or hangs. Make sure Docker is running and has enough memory, then run kind delete cluster and create it again. Run docker ps to see whether the kind-control-plane container exists.
  • Pods stay in Pending. Run kubectl describe pod -l app=web and read the Events at the bottom. On a small laptop the usual cause is not enough memory for the requests; lower the replicas or give Docker more memory.
  • Pods show CreateContainerConfigError. A referenced ConfigMap or Secret is missing or a key name is wrong. Compare kubectl get configmap,secret with the names in app.yaml.
  • The Ingress has no address. Check that cloud-provider-kind is still running in its terminal and printing no errors, and that you removed the load balancer exclusion label from the node.
  • curl to the Ingress IP times out on macOS or Windows. That IP is inside Docker’s virtual machine. Use localhost and the port of the kindccm- container instead.

Clean up

Warning: deleting the cluster removes everything in it, permanently. Check first which clusters exist so you delete the right one:

kind get clusters
kind delete cluster

kind delete cluster deletes the cluster named kind; add --name for another name. Then stop cloud-provider-kind with Ctrl+C and check that no helper containers are left with docker ps --filter name=kindccm. You can delete app.yaml, ingress.yaml and index.html if you no longer need them.

Next steps

  • Package the same app as a Helm chart so you can install it with different settings per environment.
  • Build your own image, load it into the cluster with kind load docker-image my-app:latest and deploy it instead of nginx.
  • Create a multi-node cluster with a kind configuration file and watch how Pods spread across nodes.
  • Add a PersistentVolumeClaim and see what happens to data when a Pod is replaced.
  • Replace the Ingress with Gateway API resources, which Cloud Provider KIND also supports.

Official documentation

See also

  • 0131 Kubernetes architecture: control plane, nodes, kubelet and etcd A Kubernetes cluster is a control plane that stores and decides the desired state, plus worker nodes that run your containers. This… Beginner · Concept · Linux Article Concepts 15 Oct 2026
  • 0132 Pods, ReplicaSets and Deployments explained A Pod runs one or more containers together, a ReplicaSet keeps a fixed number of identical Pods alive, and a Deployment manages… Beginner · Concept · Linux Article Concepts · Workloads & services 15 Oct 2026
  • 0135 Deploy your first app on Kubernetes A step-by-step tutorial: write a Deployment and a Service in YAML, apply them, reach the app, add health probes, roll out a… Beginner · How-to · Linux Article Ingress & networking · Workloads & services 15 Oct 2026
  • 0136 Persistent storage in Kubernetes: PersistentVolumes, claims and StorageClasses How to give a Pod storage that survives restarts: request it with a PersistentVolumeClaim, let a StorageClass provision it, and mount it.… Intermediate · How-to · Linux Article Config, secrets & storage · Workloads & services 15 Oct 2026
  • 0138 Kubernetes RBAC basics: least-privilege access for a service account RBAC controls who may do what in a cluster with Roles, ClusterRoles and bindings. This guide grants a CI service account deploy… Advanced · How-to · Linux Article Concepts · Config, secrets & storage 15 Oct 2026
  • 0140 kubectl cheat sheet: the commands you use every day A compact reference of everyday kubectl commands: contexts and namespaces, listing and inspecting objects, logs and exec, applying changes, rollouts, scaling and… Beginner · Reference · Any system Article Concepts · Troubleshooting 15 Oct 2026

Repos in the wild

  • kubernetes/kubernetes kubernetes/kubernetes The main source repository of Kubernetes: the API server, scheduler, controller manager, kubelet, kube-proxy and kubectl, plus release notes and issue tracking. GitHub

History

Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.

  1. Commitpedia +15,762 chars

Report a problem

Something wrong, unsafe or copied? Tell the editors.
What is wrong?