A practical, zero-to-running walkthrough: install the tools, spin up a local cluster, deploy your first app, expose and scale it, roll out a new version, and clean up. Every command runs locally — no cloud account needed.
| You need | Why |
|---|---|
| A machine with 2+ CPUs and 4GB+ RAM free | The local cluster runs real control-plane components |
| A container/VM driver (Docker Desktop is easiest) | Minikube runs the node inside it |
| A terminal | Everything here is CLI |
You do not need a cloud account, a registry, or any paid service to follow along.
kubectlkubectl is the command-line client that talks to the Kubernetes API. Install it for your OS:
# macOS (Homebrew)
brew install kubectl
# Linux
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# Verify
kubectl version --client
Minikube gives you a real single-node cluster on your laptop — perfect for learning and development (but not production, because a single node has no high availability).
# Install (macOS example)
brew install minikube
# Start a cluster, pinning a Kubernetes version close to your target
minikube start --driver=docker --kubernetes-version=v1.30.2 --cpus=4 --memory=6g
# Confirm the cluster is up
kubectl get nodes
kubectl cluster-info
Enable a couple of handy add-ons:
minikube addons enable metrics-server # powers `kubectl top`
minikube addons enable ingress # HTTP routing
Kubernetes is declarative: you describe the desired state and the cluster makes it true. Here is a minimal Deployment.
# web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels: { app: web }
template:
metadata:
labels: { app: web }
spec:
containers:
- name: web
image: nginx:1.27
ports:
- containerPort: 80
resources:
requests: { cpu: "100m", memory: "64Mi" }
limits: { cpu: "250m", memory: "128Mi" }
Apply it and watch the pods come up:
kubectl apply -f web.yaml
kubectl get pods -w # Ctrl-C once all 3 are Running
kubectl get deploy,rs,pods
Tip:
kubectl applyis idempotent — re-running it only changes what differs. Keep your YAML in Git; that becomes your source of truth.
Pod IPs change on every restart, so you never talk to a pod directly. A Service gives a stable virtual IP and load-balances across the pods.
# Create a ClusterIP Service (internal), then reach it from your host
kubectl expose deployment web --port=80 --target-port=80
kubectl get svc web
kubectl get endpoints web # the pod IPs the Service fans out to
# Open it in a browser via Minikube
minikube service web --url
For real HTTP exposure you would front many Services with a single Ingress instead of one cloud load balancer per app.
Scaling is a one-liner (imperative) or a field change (declarative):
# Imperative
kubectl scale deployment web --replicas=5
# Or edit replicas in web.yaml and re-apply (preferred, keeps Git in sync)
kubectl apply -f web.yaml
Let the cluster scale for you based on CPU:
kubectl autoscale deployment web --cpu-percent=60 --min=3 --max=10
kubectl get hpa web
Change the image and Kubernetes performs a rolling update — replacing pods gradually with zero downtime. If something is wrong, roll back to the previous revision instantly.
# Roll forward
kubectl set image deployment/web web=nginx:1.27-alpine
kubectl rollout status deployment/web
# Inspect history
kubectl rollout history deployment/web
# Roll back the moment you spot trouble
kubectl rollout undo deployment/web
Rollback is your fastest lever during an incident — practice it before you need it.
Keep this within reach while you learn.
| Task | Command |
|---|---|
| List everything in a namespace | kubectl get all |
| Detailed status + events | kubectl describe pod <pod> |
| App logs (and the crash before a restart) | kubectl logs <pod> / kubectl logs <pod> --previous |
| Shell into a container | kubectl exec -it <pod> -- sh |
| Debug with an ephemeral container | kubectl debug -it <pod> --image=busybox |
| Resource usage | kubectl top pod / kubectl top node |
| Preview a change before applying | kubectl diff -f web.yaml |
| Watch resources live | kubectl get pods -w |
| Port-forward for local testing | kubectl port-forward svc/web 8080:80 |
| Explain any field | kubectl explain deployment.spec.strategy |
When a pod misbehaves, gather evidence in this order:
kubectl describe pod <pod> — read the Events at the bottom.kubectl logs <pod> --previous — the crash that caused the last restart.kubectl top pod <pod> — is it hitting a memory/CPU limit?kubectl get events --sort-by=.lastTimestamp — cluster-wide context.# Remove the app
kubectl delete -f web.yaml
kubectl delete svc web
# Tear down the whole local cluster when you're done
minikube stop # keep it for next time
minikube delete # remove it entirely
latest as a version. image: nginx:latest makes rollouts non-reproducible and rollbacks meaningless. Pin a tag.kubectl describe pod puts the real reason (ImagePull, scheduling, probe failures) at the bottom. Read it first.kubectl logs <pod> shows the new container. Use --previous to see what actually crashed.kubectl edit and forgetting Git. Your cluster and your YAML drift. Change the manifest and re-apply.web.yaml, expose it, open it via minikube service, then scale to 5 and watch the new pods schedule.nginx:1.27-alpine, then kubectl rollout undo and confirm with rollout history. Time how fast it is.nginx:doesnotexist, apply, and diagnose it using only describe → logs --previous → events. What's the pod status?--cpu-percent=60 --min=3 --max=10), generate load, and watch kubectl get hpa web change replica count.Self-check:
CrashLoopBackOff. What three commands, in what order, find the cause?You now have the core loop: declare → apply → observe → iterate. Everything else in Kubernetes builds on it.
Learned the concepts? Test yourself with Kubernetes interview questions.
Go to the question bank →