Learn Kubernetes Hands-On: 8 Free Labs From Your First Pod to a Rollout
· 7 min read · StackMonsters
You can read about Kubernetes for weeks and still freeze the first time kubectl get pods shows something you didn't expect. The concepts only stick once you've watched them happen: a Pod that doesn't come back, a ReplicaSet that replaces one, a rollout that stops halfway. This tutorial is a first hands-on session, in the order the ideas build on each other. Every step has the commands to run, what you should see, and why. Each one also links a free lab with a cluster in your browser, so you don't need to install anything.
If you'd rather run a cluster on your own machine, kind, minikube and k3d all give you one in a few minutes, and every command below works there too. For the bigger question of what to learn in which order and how to know you're ready for CKA or CKAD, see The Best Way to Learn Kubernetes in 2026.
1. Look around the cluster
Start by asking the cluster what it has:
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
control-plane Ready control-plane 12d v1.33.1
$ kubectl get pods
No resources found in default namespace.
$ kubectl get deployments -n kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
coredns 2/2 2 2 12dTwo things to notice. First, kubectl get pods only looked in the default namespace; namespaces split a cluster into separate groups of objects, and most commands take -n <namespace> or -A for all of them. Second, the cluster's own add-ons, such as DNS, are ordinary Deployments in kube-system. You'll manage your own workloads with the same tools.
2. Run your first Pod
A Pod is the smallest thing Kubernetes runs: one or more containers that share a network address and can share volumes. Create one directly:
$ kubectl run podling --image=nginx:1.27
pod/podling created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
podling 0/1 ContainerCreating 0 3s
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
podling 1/1 Running 0 9sContainerCreating is the node pulling the image and starting the container. READY 1/1 means one of one containers is ready. kubectl describe pod podling shows everything Kubernetes knows about it, and its Events section, at the bottom, is the first place to look whenever a Pod isn't doing what you expect.
3. Find things with labels
Labels are key-value pairs on any object, and they're how Kubernetes objects find each other. A Service finds its Pods by label, and a ReplicaSet counts its Pods by label. You use them the same way from the command line:
kubectl label pod podling owner=me # add a label
kubectl get pods --show-labels # see every label
kubectl get pods -A -l tier=backend # every backend Pod, in every namespace
kubectl get pods -l 'tier in (backend,legacy)' # set-based selector
kubectl delete pods -l tier=legacy # act on whatever matchesThe last line is the reason to be careful with selectors: a delete by label acts on everything that matches, not on what you meant. Run the same selector with get first.
4. Pods don't come back. Controllers bring them back.
Delete the Pod you made:
$ kubectl delete pod podling
pod "podling" deleted
$ kubectl get pods
No resources found in default namespace.It's gone for good. A bare Pod has nothing watching over it, and the same goes for a Pod on a node that dies. That's why you almost never create Pods directly. You create a controller that keeps a number of Pods running. The simplest is a ReplicaSet:
apiVersion: apps/v1
kind: ReplicaSet
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27Apply it, delete one of its Pods, and look again:
$ kubectl apply -f web-rs.yaml
replicaset.apps/web created
$ kubectl delete pod web-7xk2p
pod "web-7xk2p" deleted
$ kubectl get pods -l app=web
NAME READY STATUS RESTARTS AGE
web-hq9vd 1/1 Running 0 2m
web-m4c8z 1/1 Running 0 2m
web-s2l6n 1/1 Running 0 4sStill three, and one of them is four seconds old. The ReplicaSet doesn't remember specific Pods. It counts the Pods that match its selector and creates or deletes Pods until the count equals replicas. That loop of comparing what you asked for with what exists, and closing the gap, is how every Kubernetes controller works.
5. Deployments: scale and change without downtime
In practice you create a Deployment, which manages ReplicaSets for you. The extra layer exists for updates: when you change the Pod template, the Deployment creates a new ReplicaSet and shifts Pods over gradually.
$ kubectl create deployment market-web --image=nginx:1.27 --replicas=3
deployment.apps/market-web created
$ kubectl scale deployment market-web --replicas=5
deployment.apps/market-web scaled
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
market-web-7c5ddbdf54 5 5 5 1mScaling changes how many Pods there are, not what they look like, so there's still one ReplicaSet. Change the image and you get a second one:
$ kubectl set image deployment/market-web nginx=nginx:1.26
deployment.apps/market-web image updated
$ kubectl rollout status deployment/market-web
deployment "market-web" successfully rolled out
$ kubectl get rs
NAME DESIRED CURRENT READY AGE
market-web-6f8b9d7c48 5 5 5 20s
market-web-7c5ddbdf54 0 0 0 3mThe old ReplicaSet is kept at zero replicas. That's what makes kubectl rollout undo deployment/market-web instant: it scales the old one back up. The Pod names also tell you who owns them: market-web-7c5ddbdf54-x7wlp is Deployment, then ReplicaSet hash, then a random suffix.
How the Pods are swapped is the Deployment's strategy. The default, RollingUpdate, replaces Pods a few at a time so some are always serving. Recreate deletes every old Pod before creating new ones, which causes downtime but guarantees two versions never run at once. That matters for an application that can't share a database schema across versions.
6. Write it down: declarative YAML
So far you've created everything with commands. That's quick, but it leaves no record: nobody can review, version or recreate what you typed. The usual workflow is to let kubectl write the first draft of a manifest, then keep the file:
kubectl create deployment web --image=nginx:1.27 --replicas=3 \
--dry-run=client -o yaml > web.yaml # generates the YAML without creating anything
# edit web.yaml: add resources, probes, labels
kubectl apply -f web.yaml # create or update the cluster to match the filekubectl apply is idempotent: run it again with the same file and nothing changes; run it with an edited file and only the difference is applied. When you don't remember a field, kubectl explain deployment.spec.strategy documents it from the cluster's own schema, which is faster than searching and always matches your cluster's version.
7. Catch drift before it catches you
The file is only the truth until someone changes the cluster by hand. Someone scales up during an incident and forgets to scale back:
$ kubectl scale deployment market-web --replicas=5
$ kubectl diff -f market-web.yaml
...
- replicas: 5
+ replicas: 2
...kubectl diff shows what apply would change, so it's how you notice the live cluster no longer matches the file. Here, applying the file brings the Deployment back to 2 and the extra Pods terminate. Whether that's the right move is a real decision. If the extra replicas were needed, the fix is to change the file, not the cluster. Teams that use GitOps tools like Argo CD or Flux automate exactly this comparison.
What you've practised, and what's next
You've seen the core loop Kubernetes runs on: you declare what you want, and controllers keep making the cluster match it. Pods are disposable, ReplicaSets keep a count, Deployments manage changes, and labels connect all of it.
The next skill is finding out why that loop isn't converging: a Pod in CrashLoopBackOff, an image that won't pull, a rollout that stops halfway. Kubernetes Troubleshooting Labs walks through those failures with a broken cluster for each one. After that come Services and DNS, configuration with ConfigMaps and Secrets, health checks, and storage.
