co-powered by
No experience needed

Kubernetes, Hands-On

Six short steps in one hour. Build a cluster, run an app on it, open it in your browser, scale it, update it, and fix it when it breaks.

⏱ 60 minutes 🐳 Pre-req: Docker Lab

Do not worry. Your whole cluster is three Docker containers on your laptop. The last command deletes them. One step breaks on purpose, so you can see what a real error looks like. Nothing here can harm your computer.

πŸ“˜ Want to go deeper later? Kubernetes, Deep Dive adds the scheduler, taints, draining a node, health probes and the REST API. It uses this same cluster, so do this hour first and nothing will need rebuilding.

The plan

MinStepWhat you can say after it
0–10Setup"I built a Kubernetes cluster on my laptop"
10–151 Look at your cluster"I know what my three machines do"
15–262 Run your first app"I wrote one file and Kubernetes ran it on two machines"
26–373 Open it in a browser"My app is on a real port"
37–444 Scale it, then kill it"I deleted my running app and nothing went down"
44–525 Update, redeploy, undo"I shipped a new version and took it back"
52–606 Break it and fix it"I can read an error and find the cause"

First: what is Kubernetes?

Kubernetes is a program you give a wish to. It then works non-stop to make that wish true.

You do not tell it how. You tell it what you want: "three copies of this app should run". It checks again and again, forever, and fixes things when they are wrong.

🌑️ Think of a thermostat. You do not switch the heater on and off all evening. You set a dial to 22°C and walk away. The thermostat reads the room, compares it to 22, and acts.

Kubernetes is a thermostat for software. You set the dial: "three copies of nginx". It checks the cluster, compares, and acts. Delete a copy and it makes a new one.

In step 4 you will delete a running app. A new one will appear in about two seconds. Nobody will ask you anything.

This is the big difference from Docker. docker run is an order: do this, once, now. A Kubernetes file is a standing instruction: keep this true, forever.

You, in the terminal kubectl apply -f app.yaml Control plane node api-server Β· etcd scheduler Β· controllers decides, records, never sleeps Worker node 1 Pod Pod Worker node 2 Pod room for more wishes places each Pod on whichever node has room
Your cluster today. The left box is you; everything else is three Docker containers pretending to be three servers.

Seven words you will hear all day

WordWhat it means
ClusterA group of machines managed as one. Yours will have three.
NodeOne machine. Yours are Docker containers acting as servers.
Control planeThe node that thinks. It stores your wishes and decides where things go. It does not run your apps.
WorkerA node that runs your apps. More workers means more space.
PodOne running copy of your app. Pods are disposable. They get replaced often. That is normal.
DeploymentYour instruction: "keep N copies of this image running". This is the file you write.
ServiceOne fixed address in front of Pods that keep changing.
⚠️ Every command runs in an Ubuntu (WSL) terminal, not PowerShell. If your prompt shows PS C:\Users\..., type wsl and press Enter. You will need two terminal tabs today. In Windows Terminal, press Ctrl+Shift+T for a new tab.

Setup 10 minutes

You need three tools. Check what you already have:

docker ps
kubectl version --client
kind version

If docker ps gives an error, start Docker first. If the other two say "command not found", open the box below.

🧰 Install kubectl and kind (2 minutes)
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && rm kubectl

curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.30.0/kind-linux-amd64
sudo install -o root -g root -m 0755 kind /usr/local/bin/kind && rm kind

kubectl version --client && kind version

On an ARM laptop or an Apple Silicon Mac, change amd64 to arm64 in both links.

Make a folder for today and stay inside it:

mkdir -p ~/k8s-lab && cd ~/k8s-lab

Build the cluster

You will build one control plane and two workers. You will also open ports 30000 to 30050, so your browser can reach apps inside the cluster later.

Copy this whole block at once. It writes the config file for you, then builds the cluster:

{
  echo 'kind: Cluster'
  echo 'apiVersion: kind.x-k8s.io/v1alpha4'
  echo 'name: k8s-lab'
  echo 'nodes:'
  echo '  - role: control-plane'
  echo '    extraPortMappings:'
  for p in $(seq 30000 30050); do
    echo "      - containerPort: $p"
    echo "        hostPort: $p"
    echo "        protocol: TCP"
  done
  echo '  - role: worker'
  echo '  - role: worker'
} > kind-config.yaml

kind create cluster --config kind-config.yaml

The first run downloads an image, so wait two or three minutes. Then check:

kubectl get nodes
docker ps --format "{{.Names}}"
NAME                    STATUS   ROLES           AGE   VERSION
k8s-lab-control-plane   Ready    control-plane   2m    v1.34.0
k8s-lab-worker          Ready    <none>          1m    v1.34.0
k8s-lab-worker2         Ready    <none>          1m    v1.34.0

k8s-lab-control-plane
k8s-lab-worker
k8s-lab-worker2

πŸ’¬ Read both outputs together. Kubernetes says you have three nodes. Docker says you have three containers. They are the same three things.

kind means "Kubernetes IN Docker". It runs a full cluster inside containers, so you can learn on a laptop. This is also why cleanup is fast: deleting the cluster means deleting three containers.

If setup fails

What you seeWhat to do
Cannot connect to the Docker daemonStart Docker: sudo systemctl start docker, or open Docker Desktop
C:\ in your promptType wsl and press Enter
It stops at "Joining worker nodes"Not enough memory. Run kind delete cluster --name k8s-lab, delete one - role: worker line, try again
port is already allocatedAnother program uses a port in that range. Change seq 30000 30050 to seq 30010 30030 and try again
connection to the server was refusedThe containers stopped. This is normal after a restart: docker start k8s-lab-control-plane k8s-lab-worker k8s-lab-worker2
⚠️ Do not spend more than 10 minutes here. If your laptop will not cooperate, work with a partner and move on. You can rebuild the cluster later with two commands.
1

Look at your cluster

Meet your three machines, and see Kubernetes running itself.

~5 min🟒 Easy
kubectl config current-context
kubectl get nodes -o wide
kubectl get pods
kind-k8s-lab

NAME                    STATUS   ROLES           AGE   VERSION   INTERNAL-IP
k8s-lab-control-plane   Ready    control-plane   8m    v1.34.0   172.18.0.4
k8s-lab-worker          Ready    <none>          7m    v1.34.0   172.18.0.2
k8s-lab-worker2         Ready    <none>          7m    v1.34.0   172.18.0.3

No resources found in default namespace.

"No resources found" is correct, not an error. You have not deployed anything yet.

The context tells kubectl which cluster to talk to. You have only one today. In a job you will have three or four, including production. Check your context before you run anything that deletes. It takes two seconds and it prevents very bad days.

Why your apps will never run on the control plane

The control plane is healthy and has free space. Your Pods will still not go there. Here is the reason:

kubectl describe node k8s-lab-control-plane | grep -i -A1 taints
Taints:   node-role.kubernetes.io/control-plane:NoSchedule

A taint is a "keep out" sign on a node. Your app does not have a pass, so it is never placed there. This is on purpose. If your app used all the memory on the control plane, you would lose the ability to fix anything.

Kubernetes runs itself as Pods

kubectl get pods -n kube-system -o wide

A namespace is a folder inside the cluster. Your work goes in default. Kubernetes keeps its own parts in kube-system. Look at the NODE column and you will see a pattern:

PodWhereJob
kube-apiservercontrol planeThe front door. Every command you type goes here.
etcdcontrol planeThe notebook. It stores everything.
kube-schedulercontrol planeChooses which node runs each new Pod.
kube-controller-managercontrol planeThe thermostat. It compares what you want with what exists.
corednscontrol planeTurns names into IP addresses.
kube-proxy, kindnetall three nodesNetworking. One copy per node, added automatically.
⚠️ Look, but do not change anything in kube-system. That is how you break your own cluster.
2

Run your first app

Write one file. Watch Kubernetes build three things and place them on two machines.

~11 min🟑 The main one

You will run nginx, a real web server. In Docker that was docker run -d nginx. Here you write the wish down instead.

nano deployment.yaml

Type this in. YAML uses spaces, never tabs. Two spaces per level. If your editor adds tabs, fix it now.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:stable
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"

Save with Ctrl+O, Enter. Exit with Ctrl+X.

LineWhat it means
kind: DeploymentThe type of wish: "keep copies of an app running".
replicas: 3The dial. Three copies, always.
app: nginx (twice)A label, like a sticker. The Deployment uses it to find its own Pods. In step 3 the Service will use it too.
templateThe cookie cutter. Everything below it describes one Pod.
image: nginx:stableWhich image to run. Same idea as Docker.
requestsHow much CPU and memory to reserve. 50m means a very small slice of one CPU.
⚠️ The two app: nginx labels must match exactly. Kubernetes compares labels character by character. app: nginx and app: nginx-web are different things to it, and a mismatch fails silently.

Check the file first, then apply it. Then ask when it is ready instead of guessing:

kubectl apply --dry-run=client -f deployment.yaml
kubectl apply -f deployment.yaml
kubectl wait --for=condition=Available deployment/nginx --timeout=120s
kubectl get pods -o wide
deployment.apps/nginx created
deployment.apps/nginx condition met

NAME                     READY   STATUS    RESTARTS   AGE   IP           NODE
nginx-7c9f4d8b6d-k2xvp   1/1     Running   0          20s   10.244.1.4   k8s-lab-worker
nginx-7c9f4d8b6d-m8ztq   1/1     Running   0          20s   10.244.2.3   k8s-lab-worker2
nginx-7c9f4d8b6d-p4wnc   1/1     Running   0          20s   10.244.1.5   k8s-lab-worker
⚠️ Sample outputs on this page are examples only. Your names, IP addresses and ages will be different. Match the shape of the output, not the characters.

πŸ’¬ Look at the NODE column. You never chose a machine, and your app is on two of them. The scheduler looked at your cluster, found space, and decided. You only said what you wanted.

One file, three objects

kubectl get deployments
kubectl get replicasets
kubectl get pods

Read the three names. The whole story is in them:

Deployment:   nginx
ReplicaSet:   nginx-7c9f4d8b6d
Pod:          nginx-7c9f4d8b6d-k2xvp
Deployment manages versions β€” "nginx:stable, Γ—3" ReplicaSet manages the count β€” "exactly 3 must exist" k8s-lab-worker Pod Β· 10.244.1.4 Pod Β· 10.244.1.5 k8s-lab-worker2 Pod Β· 10.244.2.3 room for more creates
You wrote the top box. The controllers built the rest, and the scheduler decided which grey box each Pod landed in.

You created one thing. Kubernetes built the other two and keeps them correct, forever.

πŸ’¬ That code in the middle name, 7c9f4d8b6d, comes from your Pod template. Write yours down. In step 5 you will change the image, and a new ReplicaSet with a different code will appear. That is how updates and rollbacks work.

What happened when you pressed Enter

This is worth 60 seconds, because it explains most confusing errors later.

kubectl an HTTPS client kube-apiserver authenticate Β· authorise admit Β· validate etcd written down 201 Created β€” your command is finished here, nothing is running yet everything below just watches the api-server and reacts controllers Deployment β†’ ReplicaSet β†’ Pod objects scheduler picks a node that fits kubelet on that node takes the job containerd pulls the image, starts the container
Nobody in the bottom row talks to anybody else. They each watch the API server and act on what they see.
  1. kubectl sends your file to the API server over HTTPS.
  2. The API server checks who you are, checks you are allowed, and saves it in etcd. Then it replies created.
  3. Your command is finished here. Nothing is running yet.
  4. Controllers notice the new Deployment and create a ReplicaSet, then Pods with no node.
  5. The scheduler picks a node for each Pod. The kubelet on that node starts the container.

Nobody in that list talks to anybody else. They all watch the API server and react. This is why created means "your wish was saved", not "your app is running", and why you used kubectl wait above.

You can see the handover written down:

kubectl describe deployment nginx | tail -n 8

That Events block is the object's diary. When something breaks later today, look here first.

3

Open it in a browser

Give three moving Pods one fixed address, then reach it from Windows on a real port.

~11 min🟒 Easythe payoff

Those Pod IP addresses disappear when the Pod does. You will see that in step 4. You cannot build anything on an address that disappears. That is why we need a Service.

nano service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  selector:
    app: nginx
  ports:
    - protocol: TCP
      port: 80          # the Service's port inside the cluster
      targetPort: 80    # the port inside the container
      nodePort: 30000   # the port you type in your browser

Note apiVersion: v1 here, but apps/v1 in the Deployment. Services are older and belong to the core group. Copying the wrong one gives a confusing error.

⚠️ selector: app: nginx must match the label in deployment.yaml exactly. If it does not match, the Service accepts connections and sends them nowhere. Nothing turns red. This is the most common Kubernetes bug there is.
kubectl apply -f service.yaml
kubectl get services
kubectl get endpoints nginx
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        18m
nginx        NodePort    10.96.142.88   <none>        80:30000/TCP   4s

NAME    ENDPOINTS                                   AGE
nginx   10.244.1.4:80,10.244.1.5:80,10.244.2.3:80   4s

πŸ’¬ Stop at the ENDPOINTS line. It must show three IP addresses, one per Pod. This proves your labels matched. If it shows <none>, fix the labels now. Nothing below will work.

Nobody writes that list by hand. A controller builds it by matching labels, and updates it whenever a Pod appears or disappears.

Now open your normal browser on Windows, Chrome or Edge:

http://localhost:30000

You should see the "Welcome to nginx!" page. πŸŽ‰ No tunnel. No terminal held open. You could close Ubuntu and the page would still load.

Your browser localhost:30000 control-plane node nodePort 30000 open kube-proxy forwards (runs none of your Pods) Service port 80 picks by label app=nginx Pod 10.244.1.4:80 on k8s-lab-worker Pod 10.244.2.3:80 on k8s-lab-worker2 targetPort 80 β€” the port inside the container
You knock on a machine that runs none of your app, and still get an answer. That hop is kube-proxy's whole reason to exist.

Three ports, and people mix them up

FieldWhere it livesWhat it is
nodePort: 30000every nodeThe outside door. It must be between 30000 and 32767. Your setup opened 30000 to 30050, so pick from that range today.
port: 80the ServiceThe port other Pods inside the cluster use.
targetPort: 80the PodThe port the container listens on.

Your request reached a Pod on a worker, but you knocked on the control plane. A NodePort opens on every node, and Kubernetes forwards it to whichever node has a Pod.

The other way in: a quick tunnel

Sometimes you just want to look at an app without opening a port. Open your second terminal:

kubectl port-forward service/nginx 8080:80

The command looks frozen. That is correct. It is holding a tunnel open. Visit http://localhost:8080 and you get the same page. Press Ctrl+C to stop it.

port-forwardNodePort
Who can reach itOnly your laptop, only while the command runsAnyone who can reach the machine
Shared across all PodsNo. It sticks to one PodYes. Requests are spread across all of them
Use it forA quick look at one thingDemos, and anything another person opens
4

Scale it, then kill it

Turn the dial up, then delete a running app and watch nothing bad happen.

~7 min🟒 Easythe fun one

Scale up

kubectl scale deployment nginx --replicas=5
kubectl get pods -o wide
kubectl get endpoints nginx

Two new Pods. The old three were not restarted. Kubernetes only created what was missing.

The Service picked up the new Pods on its own. You did not touch service.yaml. It matches by label, so any new Pod with app=nginx joins as soon as it is ready. Go back to three:

kubectl scale deployment nginx --replicas=3

Now delete one on purpose

In your second terminal, start watching:

kubectl get pods -o wide -w

In the first terminal, copy a real Pod name from your output and delete it:

kubectl get pods
kubectl delete pod <paste-one-of-your-pod-names>
nginx-7c9f4d8b6d-k2xvp   1/1   Terminating         0   8m    k8s-lab-worker
nginx-7c9f4d8b6d-z9rtb   0/1   Pending             0   0s    <none>
nginx-7c9f4d8b6d-z9rtb   0/1   ContainerCreating   0   0s    k8s-lab-worker2
nginx-7c9f4d8b6d-z9rtb   1/1   Running             0   2s    k8s-lab-worker2

A new Pod appeared in about two seconds. It has a new name, a new IP, and it may be on a different machine. Press Ctrl+C to stop watching.

Now reload http://localhost:30000. The site never went down. The other two Pods served every request while this happened.

πŸ’¬ That is the thermostat working. The ReplicaSet counted two Pods. It wanted three. So it made one more. Nobody was called.

The new Pod is a different Pod, not the old one started again. New name, new IP, no memory of the old one. This is why we say Pods are disposable, and why you must never write a Pod IP into your code. The Service exists because Pod IPs always change.

⚠️ So how do you really delete this app? Not by deleting Pods. You just saw that fail. You change the wish: kubectl delete deployment nginx. The instruction is permanent. The Pods are temporary. Do not run it yet.
5

Update, redeploy, undo

Ship a new version with no downtime. Force a redeploy when nothing changed. Then take it all back.

~8 min🟑 Medium

5.1 β€” Ship a new version

kubectl set image deployment/nginx nginx=nginx:stable-alpine
kubectl rollout status deployment/nginx

Reload http://localhost:30000 while it runs. The site stays up. New Pods start and are checked before old ones are removed.

In that command, deployment/nginx is the Deployment name and nginx= is the container name from your file. They are the same word here by chance.

There are two common ways to ship a new version:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: burgxy/webapps:v4
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "250m"
              memory: "164Mi"
kubectl apply -f deployment.yaml
kubectl rollout status deployment/nginx

This declarative approach is often preferred in real projects because the change is stored in Git, is easy to review, and is easy to reproduce.

kubectl get replicasets
NAME               DESIRED   CURRENT   READY   AGE
nginx-5d7b9c4f8a   3         3         3       40s
nginx-7c9f4d8b6d   0         0         0       14m

Two ReplicaSets now, with a new code in the name. The new one runs your Pods. The old one is empty but kept. That empty one is your undo button:

kubectl rollout undo deployment/nginx
kubectl rollout status deployment/nginx
kubectl get deployment nginx -o jsonpath='{.spec.template.spec.containers[0].image}'; echo

Back to nginx:stable in about two seconds. Nothing was rebuilt or downloaded again, because the old ReplicaSet was never deleted.

5.2 β€” Redeploy when nothing changed

This is the job you will do most often at work. Try applying the file you have not edited:

kubectl apply -f deployment.yaml
deployment.apps/nginx unchanged

unchanged is not a warning. apply compares your file with the cluster and only acts on differences. No difference means no action.

But sometimes you do want all Pods replaced. Maybe you rebuilt the image with the same tag. Maybe a setting changed. Note the Pod names first, so you can compare:

kubectl get pods -l app=nginx          # before
kubectl rollout restart deployment/nginx
kubectl rollout status deployment/nginx
kubectl get pods -l app=nginx          # after
NAME                     READY   STATUS    RESTARTS   AGE
nginx-7c9f4d8b6d-m8ztq   1/1     Running   0          14m
nginx-7c9f4d8b6d-p4wnc   1/1     Running   0          14m
nginx-7c9f4d8b6d-t8kqn   1/1     Running   0          14m

deployment.apps/nginx restarted
deployment "nginx" successfully rolled out

NAME                     READY   STATUS    RESTARTS   AGE
nginx-8a3e1c7d25-6rzv2   1/1     Running   0          21s
nginx-8a3e1c7d25-hq4nd   1/1     Running   0          17s
nginx-8a3e1c7d25-xp9ck   1/1     Running   0          13s
ReadingBeforeAfter
Imagenginx:stablenginx:stable β€” the same
Your YAML fileNot changed at all
Pod namesnginx-7c9f4d8b6d-…nginx-8a3e1c7d25-… β€” all new
AGE14m13 to 21 seconds, a few seconds apart
RESTARTS00

πŸ’¬ Look at the RESTARTS column: still 0. Nothing restarted. The old Pods were deleted and new Pods were created, a few seconds apart, one at a time. "Restart" is a misleading name. Pods are replaced, never restarted in place.

The ages are a few seconds apart on purpose. That is the rolling update doing one Pod at a time, which is why the site stayed up.

⚠️ Do not redeploy by deleting all the Pods yourself. kubectl delete pod -l app=nginx removes every copy at once, so for a few seconds there is nothing to answer requests. Users see errors. rollout restart reaches the same result with no downtime.
⚠️ One honest warning about undo. rollout undo brings back old containers. It does not undo database changes, emails already sent, or data already written. Also, a successful rollout does not mean a working app. Pods can be Running while every request fails. Always check behaviour, not just status.
6

Break it and fix it

Cause a real failure, find the cause in 30 seconds, and repair it.

~8 minπŸ”΄ The useful one

6.1 β€” Break it

kubectl set image deployment/nginx nginx=nginx:this-tag-does-not-exist

Wait about 20 seconds, then:

kubectl get pods
curl -s http://localhost:30000 | head -n 1
NAME                     READY   STATUS             RESTARTS   AGE
nginx-8a3e1c7d25-6rzv2   1/1     Running            0          6m
nginx-8a3e1c7d25-hq4nd   1/1     Running            0          6m
nginx-6b4d9f2c1e-t5qmx   0/1     ImagePullBackOff   0          22s

<!DOCTYPE html>

πŸ’¬ See what Kubernetes did before you even looked. The old Pods are still running and still serving. The update stopped instead of destroying a working app. Reload the browser. The site is fine.

A failed deployment is not automatically an outage.

6.2 β€” Find out why

Two commands. Knowing which one to use is the real skill. Use your broken Pod name:

kubectl describe pod <your-broken-pod-name>

Scroll to Events at the bottom:

Normal   Scheduled  45s  default-scheduler  Successfully assigned default/nginx-6b4d9f2c1e-t5qmx to k8s-lab-worker
Normal   Pulling    44s  kubelet            Pulling image "nginx:this-tag-does-not-exist"
Warning  Failed     42s  kubelet            Failed to pull image: not found
Warning  Failed     15s  kubelet            Error: ImagePullBackOff

The answer is right there in plain English: the image tag does not exist. You can also see which part failed. The scheduler worked. The kubelet could not pull the image.

Now try the other command:

kubectl logs <your-broken-pod-name>
Error from server (BadRequest): container "nginx" is waiting to start:
trying and failing to pull image

πŸ’¬ This is the lesson of the step. logs had nothing to show, because the container never started. You cannot read the output of a program that does not exist.

Use describe when it never started. Use logs when it started but behaves wrongly. Choosing the right one is the difference between a two-minute fix and a lost afternoon.

Also notice: that red error message was useful information, not a wall. Read errors. Do not panic at them.

6.3 β€” Fix it

kubectl rollout undo deployment/nginx
kubectl rollout status deployment/nginx
kubectl get pods -o wide
kubectl get endpoints nginx

You must finish with: 3/3, three Pods Running, three endpoints, and http://localhost:30000 still showing the nginx page.

What each status means

StatusWhat is wrongFirst command
PendingNo machine had space for itkubectl describe pod <name>
ContainerCreatingStill pulling the image. Normal for a few secondsWait. If stuck, describe
ImagePullBackOffWrong image name or tagkubectl describe pod <name>
CrashLoopBackOffIt starts, then keeps dying. Usually your codekubectl logs <name> --previous
Running but 0/1Alive, but not ready for traffic yetkubectl describe pod <name>
Running, users see errorsUsually a label mismatch, so no endpointskubectl get endpoints nginx
⚠️ The one rule of debugging: change one thing, then check. Never try five fixes at once. If it starts working, you will not know which one worked.
βœ“

What you learned

Six steps, six sentences. If you can say these, you have got it.

  1. Kubernetes is a thermostat for software. You write down a wish, and it works forever to keep it true.
  2. You have one control plane and two workers. A taint keeps your apps off the control plane, so a bad app cannot stop you fixing things.
  3. One file produced a Deployment, a ReplicaSet and three Pods, placed on two machines you never named. created means "saved", not "running".
  4. A Service is one fixed address, and it finds Pods by label. kubectl get endpoints is the proof it worked.
  5. Deleting a Pod does not delete an app. Only changing the desired state does. To redeploy with no file change, use rollout restart β€” Pods are replaced, never restarted.
  6. Use describe when it never started, logs when it started but is wrong. A failed update is not automatically an outage.

Commands worth keeping

When you want to…Run
Check which cluster you are on (always do this first)kubectl config current-context
See everything you madekubectl get all
See Pods with IPs and machineskubectl get pods -o wide
Watch changes livekubectl get pods -w
Find out why something is unhappykubectl describe pod NAME
Read an app's outputkubectl logs NAME
Check a Service found its Podskubectl get endpoints NAME
Change the number of copieskubectl scale deployment NAME --replicas=N
Ship a new imagekubectl set image deployment/NAME container=image:tag
Replace all Pods safelykubectl rollout restart deployment/NAME
Undo the last changekubectl rollout undo deployment/NAME
Check a YAML file before applyingkubectl apply --dry-run=client -f file.yaml
Look up any fieldkubectl explain deployment.spec.replicas

If something goes wrong

What you seeWhat to do
connection to the server was refuseddocker start k8s-lab-control-plane k8s-lab-worker k8s-lab-worker2, wait 60 seconds
error validating data: unknown fieldYAML spacing, usually tabs. Run kubectl apply --dry-run=client -f deployment.yaml to see the line
mapping values are not allowedMissing space after a colon. YAML needs key: value
Endpoints show <none>Your labels do not match. Compare them letter by letter
localhost:30000 refuses to connectRun docker port k8s-lab-control-plane | grep 30000. Nothing? Your cluster was built without the config file
Browser cannot reach localhost at all (WSL)Run hostname -I | awk '{print $1}', then open http://<that-ip>:30000
A Pod is stuck in Pendingkubectl describe pod NAME. The Events line says exactly why
Commands "not found" that worked beforeYou are in PowerShell. Type wsl
Everything is very slowYour files are under /mnt/c/. Run cd ~ and work there
πŸ†˜ Completely lost? Reset and carry on

This removes your app but keeps the cluster, so you can restart any step:

kubectl delete deployment nginx --ignore-not-found
kubectl delete service nginx --ignore-not-found
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl wait --for=condition=Available deployment/nginx --timeout=120s

Check yourself

Try to answer out loud before opening each one.

Q1 β€” You applied a Deployment with replicas: 3. Which objects exist now, and who made them?

A Deployment, one ReplicaSet, and three Pods, each running one container. The Deployment made the ReplicaSet. The ReplicaSet made the Pods. You wrote only the Deployment. Controllers did the rest.

Q2 β€” You deleted a Pod. Thirty seconds later there are still three. Why?

Because the desired state is still three. The ReplicaSet controller saw two, wanted three, and made one more. It is a new Pod with a new name and new IP, not the old one started again. To really remove the app you must change the wish: kubectl delete deployment nginx.

Q3 β€” Your Service says app: nginx-web but your Pods say app: nginx. Every Pod is Running. What does the user see?

A connection error or a timeout. The Service has zero endpoints, so there is nothing to send traffic to. Labels must match exactly, and the failure is silent because the Pods really are healthy. Fastest check: kubectl get endpoints nginx shows <none>.

Q4 β€” You ran kubectl rollout restart. The Pod names changed but RESTARTS is still 0. Is that correct?

Yes, and this is the point. Nothing restarted. The old Pods were deleted and new Pods were created, one at a time. If a container had truly restarted in place, RESTARTS would be 1 and the Pod name would be the same. The image and your YAML file did not change at all.

Q5 β€” A new Pod is in ImagePullBackOff. Why do users still get a working site, and how do you recover?

Because new Pods must become healthy before old ones are removed. The new Pod never became healthy, so the update stopped and the old Pods kept serving. The Service also never added the broken Pod. Recover with kubectl rollout undo deployment/nginx.

🧹

Cleanup

Two options. Choose based on whether you will continue later.

Option A β€” keep the cluster, remove the app

kubectl delete -f service.yaml
kubectl delete -f deployment.yaml
kubectl get all

Only the built-in kubernetes Service should remain. Notice you never deleted a Pod or a ReplicaSet. Deleting the Deployment removed everything it owned. That is the chain from step 2, working backwards.

Option B β€” delete the whole cluster

kind delete cluster --name k8s-lab
kind get clusters      # prints nothing

This deletes all three containers. Everything inside is gone. Your YAML files on disk are safe, so you can rebuild the whole lab in about four minutes.

πŸ’¬ Keep your three files. kind-config.yaml, deployment.yaml and service.yaml are the real result of today. Not the Pods, which were always disposable. Not the cluster, which you can rebuild from a file. Commit them to Git.

🏠

Container to Kubernetes: Hands-On Assignment

Take the app you built in Docker and deploy it on Kubernetes.

Earlier in the Docker practical, you built the CampusConnect app in the Docker section. That app was created as a Python Flask service and ran locally with a Docker image. Now your task is to take that same image and deploy it on Kubernetes.

Open the Docker Practical Lab: https://labs.docker.learnwithdutta.com/

This is the real next step: Docker builds the image, Kubernetes runs it at scale.

  1. Go to the Docker practical and identify the Flask app you built there. It is the same project that used FROM python:3.12-slim, EXPOSE 8000, and ran python app.py.
  2. Build or retag the image again if needed, for example: docker build -t campusconnect:v1 -f Dockerfile.v1 .
  3. Make sure the image is available to your Kubernetes cluster (for example, by using a local image or a registry if your setup supports it).
  4. Create a deployment.yaml for the Python app, with the correct container image and port mapping.
  5. Deploy it with kubectl apply -f deployment.yaml and check the rollout with kubectl rollout status deployment/<name>.
  6. Test the app from the cluster and confirm the service is reachable.

Goal: Take the app you built in the Docker practical and run it as a Kubernetes Deployment, just like a real production workflow.