Kubernetes (K8s) is the industry-standard platform for deploying, scaling, and managing containerised applications. It runs on every major cloud provider, powers most production microservices systems, and is the expected skill for any DevOps, platform engineering, or senior backend role. This tutorial takes you from zero to deploying and scaling real applications in Kubernetes, step by step, with working YAML examples throughout.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Core concepts | Understand what K8s solves and how it works |
| Setup | Run K8s locally with kind or minikube |
| Pods | Create, inspect, and debug the smallest K8s unit |
| Deployments | Run and update multi-replica apps reliably |
| Services | Expose apps internally and to the outside world |
| ConfigMaps & Secrets | Manage configuration and sensitive data |
| Namespaces | Organise resources in multi-team clusters |
| Scaling | Scale up/down manually and automatically |
| Storage | Persist data with PersistentVolumes |
| 3 real projects | Deploy a web app, add a database, and set up autoscaling |
Why learn Kubernetes?
Before diving in, here's why Kubernetes matters in 2025:
| Problem without K8s | How K8s solves it |
|---|---|
| Container crashes bring down your app | K8s automatically restarts failed containers |
| Traffic spikes overwhelm a single instance | K8s scales pods horizontally in seconds |
| Deploying updates causes downtime | Rolling updates replace pods one at a time |
| Hard to run many services together | K8s schedules containers across a cluster of machines |
| Config and secrets mixed into images | ConfigMaps and Secrets separate config from code |
| Manual load balancing | Services provide built-in DNS and load balancing |
| Lost data when containers restart | PersistentVolumes survive container lifecycle |
Kubernetes vs. alternatives
| Tool | What it does | When to use |
|---|---|---|
| Docker Compose | Multi-container on a single machine | Local dev, tiny deployments |
| Docker Swarm | Simple cluster orchestration | Small teams, low complexity |
| Kubernetes | Production-grade container orchestration | Scale, HA, large teams |
| Nomad | Multi-workload scheduler (containers + VMs + binaries) | Mixed workloads |
| ECS (AWS) | Managed containers without K8s | AWS-only, simpler ops |
Part 1: How Kubernetes works
The cluster model
A Kubernetes cluster has two types of machines:
┌─────────────────────────────────────────────────────────┐
│ KUBERNETES CLUSTER │
│ │
│ ┌─────────────────────┐ ┌────────────────────────┐ │
│ │ Control Plane │ │ Worker Nodes │ │
│ │ ┌───────────────┐ │ │ ┌──────────────────┐ │ │
│ │ │ API Server │ │ │ │ Pod (container) │ │ │
│ │ │ Scheduler │◄─┼───┼──│ Pod (container) │ │ │
│ │ │ etcd │ │ │ │ Pod (container) │ │ │
│ │ │ Controller │ │ │ └──────────────────┘ │ │
│ │ │ Manager │ │ │ kubelet + kube-proxy │ │
│ │ └───────────────┘ │ └────────────────────────┘ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Control plane — the brain. Manages state, schedules workloads, stores cluster config. Worker nodes — run your application containers inside Pods.
Core components
| Component | Role |
|---|---|
| API Server | Entry point for all K8s commands (kubectl talks to this) |
| etcd | Distributed key-value store — holds all cluster state |
| Scheduler | Decides which node a new Pod runs on |
| Controller Manager | Watches state and reconciles towards desired state |
| kubelet | Agent on each node — starts/stops containers |
| kube-proxy | Manages network rules for Service routing on each node |
| Container runtime | Runs containers — containerd (default), CRI-O |
The declarative model
The key idea in Kubernetes: you declare what you want (desired state), and Kubernetes constantly works to make reality match it.
You say: "Run 3 replicas of my app"
↓
K8s: runs 3 Pods
↓
One Pod crashes
↓
K8s: starts a replacement automatically
Everything in K8s is a resource described in YAML and stored in etcd.
Core resource types
| Resource | What it represents |
|---|---|
| Pod | One or more containers that share network and storage |
| Deployment | Manages a set of identical Pods (replicas + rolling updates) |
| Service | Stable network endpoint for a set of Pods |
| ConfigMap | Non-secret configuration data |
| Secret | Sensitive data (passwords, tokens) stored base64-encoded |
| Namespace | Virtual cluster — isolates resources |
| PersistentVolumeClaim | Request for persistent storage |
| Ingress | HTTP/HTTPS routing rules from outside the cluster |
| HorizontalPodAutoscaler | Automatic scaling based on CPU/memory metrics |
Part 2: Setup
Option 1: kind (Kubernetes IN Docker) — recommended for local
# Install kind
# macOS
brew install kind
# Linux
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.23.0/kind-linux-amd64
chmod +x ./kind && sudo mv ./kind /usr/local/bin/kind
# Windows (PowerShell as admin)
winget install Kubernetes.kind
# Create a cluster
kind create cluster --name my-cluster
# Verify
kubectl cluster-info --context kind-my-cluster
Option 2: minikube — alternative with more features
# Install minikube
# macOS
brew install minikube
# Linux
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
# Start cluster
minikube start
# Enable dashboard (optional)
minikube dashboard
Install kubectl
kubectl is the CLI for talking to any Kubernetes cluster:
# macOS
brew install kubectl
# Linux
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
# Windows
winget install Kubernetes.kubectl
# Verify
kubectl version --client
Essential kubectl commands
# Cluster info
kubectl cluster-info
kubectl get nodes
# Current context (which cluster you're connected to)
kubectl config current-context
kubectl config get-contexts
# Apply a YAML file
kubectl apply -f file.yaml
# Get resources
kubectl get pods
kubectl get pods -n kube-system # in a specific namespace
kubectl get all # all resources in default namespace
# Describe a resource (detailed info + events)
kubectl describe pod <pod-name>
# View logs
kubectl logs <pod-name>
kubectl logs <pod-name> -f # stream logs
kubectl logs <pod-name> -c <container> # specific container in a pod
# Execute command in a container
kubectl exec -it <pod-name> -- /bin/sh
# Delete a resource
kubectl delete pod <pod-name>
kubectl delete -f file.yaml
# Watch resources update in real time
kubectl get pods -w
Part 3: Pods
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share:
- The same IP address
- The same localhost network
- The same mounted volumes
Your first Pod
Create nginx-pod.yaml:
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
# Apply
kubectl apply -f nginx-pod.yaml
# Check status
kubectl get pods
# Output:
# NAME READY STATUS RESTARTS AGE
# nginx-pod 1/1 Running 0 10s
# Describe it
kubectl describe pod nginx-pod
# Access it locally (port-forward)
kubectl port-forward pod/nginx-pod 8080:80
# Now open http://localhost:8080
# Delete it
kubectl delete pod nginx-pod
Pod lifecycle states
| Status | Meaning |
|---|---|
Pending |
Pod accepted but containers not yet started (scheduling or pulling image) |
Running |
At least one container is running |
Succeeded |
All containers exited successfully (0) — for Jobs |
Failed |
All containers exited, at least one with non-zero exit code |
CrashLoopBackOff |
Container keeps crashing and K8s is backing off restarts |
ImagePullBackOff |
Can't pull the container image |
Unknown |
Node communication lost |
Restart policies
spec:
restartPolicy: Always # default — restart on any failure
restartPolicy: OnFailure # restart only on non-zero exit
restartPolicy: Never # never restart
Multi-container Pods (sidecars)
apiVersion: v1
kind: Pod
metadata:
name: app-with-sidecar
spec:
containers:
- name: main-app
image: nginx:1.27-alpine
volumeMounts:
- name: shared-logs
mountPath: /var/log/nginx
- name: log-shipper
image: busybox:1.36
command: ["sh", "-c", "tail -f /logs/access.log"]
volumeMounts:
- name: shared-logs
mountPath: /logs
volumes:
- name: shared-logs
emptyDir: {}
Note: In practice, you rarely create Pods directly — use Deployments instead. Pods on their own are not rescheduled if a node fails.
Part 4: Deployments
A Deployment manages a set of identical Pods (a ReplicaSet under the hood). It handles:
- Keeping the desired number of replicas running
- Rolling updates with zero downtime
- Rollback to previous versions
Basic Deployment
Create nginx-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template: # Pod template
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 15
periodSeconds: 20
kubectl apply -f nginx-deployment.yaml
# Check deployment
kubectl get deployments
# Output:
# NAME READY UP-TO-DATE AVAILABLE AGE
# nginx-deployment 3/3 3 3 30s
# Check the pods it created
kubectl get pods -l app=nginx
Rolling updates
# Update the image version
kubectl set image deployment/nginx-deployment nginx=nginx:1.27
# Watch the rollout
kubectl rollout status deployment/nginx-deployment
# See rollout history
kubectl rollout history deployment/nginx-deployment
# Rollback to previous version
kubectl rollout undo deployment/nginx-deployment
# Rollback to specific version
kubectl rollout undo deployment/nginx-deployment --to-revision=1
Deployment update strategy
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # max extra pods during update (above replicas)
maxUnavailable: 0 # max pods that can be unavailable during update
| Strategy | Behaviour |
|---|---|
RollingUpdate (default) |
Replace pods gradually — zero downtime |
Recreate |
Kill all old pods, then start new ones — causes brief downtime |
Scaling
# Scale manually
kubectl scale deployment/nginx-deployment --replicas=5
# Or edit the YAML and reapply
kubectl apply -f nginx-deployment.yaml
Part 5: Services
Pods have ephemeral IPs — they change when pods restart. A Service provides a stable IP and DNS name that routes traffic to a set of Pods based on label selectors.
Service types
| Type | Accessible from | Use case |
|---|---|---|
ClusterIP |
Inside the cluster only | Internal microservice-to-microservice |
NodePort |
Outside via <NodeIP>:<NodePort> |
Dev/testing, simple external access |
LoadBalancer |
Outside via cloud load balancer | Production on managed K8s (EKS/GKE/AKS) |
ExternalName |
Maps to a DNS name | Proxy to external service |
ClusterIP Service (default)
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx # matches pods with this label
ports:
- protocol: TCP
port: 80 # Service port (what clients connect to)
targetPort: 80 # Pod port (where traffic goes)
type: ClusterIP
kubectl apply -f nginx-service.yaml
# Check service
kubectl get services
# Test it from inside the cluster
kubectl run test-pod --image=busybox:1.36 --rm -it --restart=Never -- wget -qO- http://nginx-service
NodePort Service
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080 # must be 30000-32767
type: NodePort
# With minikube
minikube service nginx-nodeport --url
# With kind — you need to configure port mapping at cluster creation
LoadBalancer Service
apiVersion: v1
kind: Service
metadata:
name: nginx-lb
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: LoadBalancer
On managed K8s (EKS, GKE, AKS), this provisions a real cloud load balancer automatically.
DNS inside the cluster
Every Service gets a DNS name: <service-name>.<namespace>.svc.cluster.local
# From another pod in the same namespace, these all work:
curl http://nginx-service
curl http://nginx-service.default
curl http://nginx-service.default.svc.cluster.local
Part 6: ConfigMaps and Secrets
ConfigMaps — non-sensitive configuration
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: "production"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
config.json: |
{
"timeout": 30,
"retries": 3
}
Use ConfigMap in a Pod:
spec:
containers:
- name: app
image: myapp:1.0
# Method 1: inject as environment variables
envFrom:
- configMapRef:
name: app-config
# Method 2: inject specific keys
env:
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: app-config
key: LOG_LEVEL
# Method 3: mount as files
volumeMounts:
- name: config-volume
mountPath: /etc/config
volumes:
- name: config-volume
configMap:
name: app-config
Secrets — sensitive data
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
stringData: # stringData: plain text (K8s encodes to base64)
DB_PASSWORD: "supersecretpassword"
DB_USER: "appuser"
# Create from command line (avoids putting secrets in files)
kubectl create secret generic db-secret \
--from-literal=DB_PASSWORD=supersecretpassword \
--from-literal=DB_USER=appuser
# View secrets (base64 encoded)
kubectl get secret db-secret -o yaml
# Decode
kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 --decode
Use Secret in a Pod:
spec:
containers:
- name: app
image: myapp:1.0
envFrom:
- secretRef:
name: db-secret
Security note: Secrets are stored in etcd base64-encoded (not encrypted by default). For production, enable etcd encryption at rest and use external secret managers (AWS Secrets Manager, HashiCorp Vault, or Sealed Secrets).
Part 7: Namespaces
Namespaces provide virtual clusters within a single physical cluster — useful for organising resources by team, environment, or application.
# List namespaces
kubectl get namespaces
# Default namespaces:
# default — where your resources go if you don't specify
# kube-system — K8s system components
# kube-public — publicly readable config
# kube-node-lease — node heartbeats
# Create a namespace
kubectl create namespace staging
# Or via YAML
kubectl apply -f - <<EOF
apiVersion: v1
kind: Namespace
metadata:
name: staging
EOF
# Deploy to a specific namespace
kubectl apply -f nginx-deployment.yaml -n staging
# Get resources in a namespace
kubectl get pods -n staging
# Get resources across all namespaces
kubectl get pods --all-namespaces
# or
kubectl get pods -A
# Set default namespace for current context
kubectl config set-context --current --namespace=staging
Resource quota per namespace
apiVersion: v1
kind: ResourceQuota
metadata:
name: staging-quota
namespace: staging
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
Part 8: Health checks
Liveness probe — is the container alive?
K8s restarts the container if this probe fails.
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 15 # wait before first probe
periodSeconds: 20 # how often to probe
failureThreshold: 3 # restart after 3 failures
Readiness probe — is the container ready for traffic?
K8s removes the Pod from Service endpoints until this probe passes.
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
successThreshold: 1
failureThreshold: 3
Probe types
| Type | How it works |
|---|---|
httpGet |
HTTP GET request — success if 200-399 |
tcpSocket |
TCP connection — success if connection opens |
exec |
Runs a command — success if exit code 0 |
grpc |
gRPC health check protocol |
# exec example
livenessProbe:
exec:
command:
- cat
- /tmp/healthy
initialDelaySeconds: 5
periodSeconds: 5
Part 9: Persistent Storage
Containers are ephemeral — data is lost when they restart. Use PersistentVolumes for data that must survive.
Key concepts
| Term | Meaning |
|---|---|
| PersistentVolume (PV) | Actual storage provisioned by admin or dynamically |
| PersistentVolumeClaim (PVC) | User's request for storage |
| StorageClass | Template for dynamic PV provisioning |
PersistentVolumeClaim (the common path)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce # mounted by one node at a time
resources:
requests:
storage: 5Gi
storageClassName: standard # use default StorageClass
Access modes
| Mode | Abbreviation | Description |
|---|---|---|
ReadWriteOnce |
RWO | One node can read/write |
ReadOnlyMany |
ROX | Many nodes can read |
ReadWriteMany |
RWX | Many nodes can read/write |
ReadWriteOncePod |
RWOP | One Pod at a time (K8s 1.22+) |
Use the PVC in a Deployment
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-secret
key: DB_PASSWORD
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
Part 10: Ingress
A Service of type LoadBalancer gives one IP per service. Ingress routes HTTP/HTTPS traffic from a single entry point to multiple services based on host/path rules.
Internet → Ingress Controller → /api/* → api-service
/app/* → frontend-service
/admin/* → admin-service
Install an Ingress controller (nginx)
# With minikube
minikube addons enable ingress
# With kind or bare cluster
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/cloud/deploy.yaml
Ingress resource
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
tls:
- hosts:
- myapp.example.com
secretName: myapp-tls-secret
Part 11: Scaling and Autoscaling
Manual scaling
kubectl scale deployment/nginx-deployment --replicas=10
HorizontalPodAutoscaler (HPA)
HPA automatically adjusts the number of Pods based on CPU or memory utilisation.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: nginx-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: nginx-deployment
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 # scale up when avg CPU > 70%
kubectl apply -f nginx-hpa.yaml
# Check HPA status
kubectl get hpa
# Output:
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
# nginx-hpa Deployment/nginx-deployment 30%/70% 2 10 3
Note: HPA requires the Metrics Server. Install it:
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
Part 12: Jobs and CronJobs
Job — run a task to completion
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
spec:
template:
spec:
containers:
- name: migrate
image: myapp:1.0
command: ["npm", "run", "migrate"]
restartPolicy: OnFailure
backoffLimit: 3 # retry up to 3 times on failure
CronJob — run on a schedule
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup-job
spec:
schedule: "0 2 * * *" # cron syntax: 2 AM daily
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: myapp:1.0
command: ["sh", "-c", "pg_dump $DATABASE_URL | gzip > /backup/backup.sql.gz"]
restartPolicy: OnFailure
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 1
Project 1: Deploy a Node.js web app
Step 1 — The app (already containerised)
We'll use a simple Node.js hello-world image. In a real project, you'd build your own image with Docker and push it to a registry.
Step 2 — Deployment
Save as webapp-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
labels:
app: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: webapp
image: gcr.io/google-samples/hello-app:1.0
ports:
- containerPort: 8080
resources:
requests:
memory: "64Mi"
cpu: "100m"
limits:
memory: "128Mi"
cpu: "200m"
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
Step 3 — Service
Save as webapp-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: webapp-service
spec:
selector:
app: webapp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
Step 4 — Deploy and test
kubectl apply -f webapp-deployment.yaml
kubectl apply -f webapp-service.yaml
# Verify
kubectl get pods -l app=webapp
kubectl get service webapp-service
# Access locally
kubectl port-forward service/webapp-service 8080:80
# Open http://localhost:8080
# Try killing a pod — K8s will restart it
kubectl delete pod $(kubectl get pods -l app=webapp -o jsonpath='{.items[0].metadata.name}')
kubectl get pods -l app=webapp -w # watch it recover
Project 2: Add a PostgreSQL database
Complete stack with ConfigMap, Secret, PVC, and Services
Save as postgres-stack.yaml:
# Secret for database credentials
apiVersion: v1
kind: Secret
metadata:
name: postgres-secret
type: Opaque
stringData:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: changeme123
POSTGRES_DB: appdb
---
# Persistent storage
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
---
# PostgreSQL Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- secretRef:
name: postgres-secret
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: postgres-data
persistentVolumeClaim:
claimName: postgres-pvc
---
# PostgreSQL Service (ClusterIP — internal only)
apiVersion: v1
kind: Service
metadata:
name: postgres-service
spec:
selector:
app: postgres
ports:
- protocol: TCP
port: 5432
targetPort: 5432
kubectl apply -f postgres-stack.yaml
# Verify
kubectl get pods -l app=postgres
kubectl get pvc postgres-pvc
# Connect to postgres
kubectl exec -it $(kubectl get pod -l app=postgres -o jsonpath='{.items[0].metadata.name}') \
-- psql -U appuser -d appdb
# From your webapp pods, connect using:
# host: postgres-service
# port: 5432
Project 3: Autoscaling setup
Full app with HPA
# webapp-with-hpa.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp-scaled
spec:
replicas: 2
selector:
matchLabels:
app: webapp-scaled
template:
metadata:
labels:
app: webapp-scaled
spec:
containers:
- name: webapp
image: gcr.io/google-samples/hello-app:1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "100m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"
---
apiVersion: v1
kind: Service
metadata:
name: webapp-scaled-svc
spec:
selector:
app: webapp-scaled
ports:
- port: 80
targetPort: 8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: webapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: webapp-scaled
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
kubectl apply -f webapp-with-hpa.yaml
# Watch HPA
kubectl get hpa -w
# Generate load to trigger autoscaling
kubectl run load-generator --image=busybox:1.36 --rm -it --restart=Never -- \
/bin/sh -c "while true; do wget -q -O- http://webapp-scaled-svc/; done"
# In another terminal, watch pods scale up
kubectl get pods -l app=webapp-scaled -w
Kubernetes YAML quick reference
Every K8s manifest has these four top-level fields:
apiVersion: apps/v1 # API group and version
kind: Deployment # resource type
metadata: # name, namespace, labels, annotations
name: my-app
namespace: default
labels:
app: my-app
version: "1.0"
spec: # desired state — different for each kind
...
Common kubectl commands reference
| Command | What it does |
|---|---|
kubectl apply -f file.yaml |
Create or update resources |
kubectl get pods |
List pods in current namespace |
kubectl get all |
List all resources |
kubectl describe pod <name> |
Detailed info and events |
kubectl logs <name> |
Print container logs |
kubectl logs <name> -f |
Stream logs |
kubectl exec -it <name> -- sh |
Shell into container |
kubectl port-forward svc/<name> 8080:80 |
Forward local port to service |
kubectl scale deploy/<name> --replicas=5 |
Scale a deployment |
kubectl rollout status deploy/<name> |
Check rollout progress |
kubectl rollout undo deploy/<name> |
Rollback deployment |
kubectl delete -f file.yaml |
Delete resources from file |
kubectl get events |
Show cluster events |
kubectl top pods |
Show resource usage per pod |
kubectl config use-context <ctx> |
Switch cluster context |
Learning path
| Stage | Topics | Suggested time |
|---|---|---|
| 1 — Foundations | Pods, Deployments, Services, kubectl | Week 1–2 |
| 2 — Config & Storage | ConfigMaps, Secrets, PVCs, Namespaces | Week 3 |
| 3 — Production patterns | Health checks, HPA, resource limits, Ingress | Week 4–5 |
| 4 — Observability | Prometheus + Grafana, loki logs, Jaeger tracing | Week 6–7 |
| 5 — Security | RBAC, NetworkPolicies, PodSecurity, Secrets management | Week 8–9 |
| 6 — Advanced | Helm, Operators, StatefulSets, GitOps (ArgoCD/Flux) | Week 10–12 |
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
| No resource limits | One pod can starve others | Always set requests and limits |
| No liveness/readiness probes | Broken pods receive traffic | Add probes to every container |
| Using Pods directly | Pod lost on node failure is not rescheduled | Use Deployments instead |
| Storing secrets in ConfigMaps | Secrets visible as plain text | Use Secret objects |
Deploying to the default namespace for everything |
Resources mix, hard to manage | Use namespaces per env/team |
latest image tag |
Unpredictable versions, no rollback | Pin to specific digest or semver tag |
| No readiness probe on startup | Traffic hits pod before it's ready | Set initialDelaySeconds or use startupProbe |
| Skipping resource quotas in shared clusters | One team uses all resources | Apply ResourceQuota per namespace |
Kubernetes vs. related terms
| Term | What it is |
|---|---|
| K8s | Short for Kubernetes (8 letters between K and s) |
| kubectl | CLI for interacting with K8s clusters |
| Helm | Package manager for K8s — bundles manifests as "charts" |
| kind | Kubernetes IN Docker — local cluster using Docker containers as nodes |
| minikube | Local single-node K8s cluster for development |
| YAML | Format used for all K8s manifests |
| etcd | Distributed key-value store that holds all K8s state |
| kubelet | Agent on each worker node that runs containers |
| kube-proxy | Network proxy on each node for Service routing |
| CRD | Custom Resource Definition — extend K8s with new resource types |
| Operator | Controller that manages complex stateful apps using CRDs |
| ArgoCD / Flux | GitOps tools — sync K8s state from a Git repository |
Frequently asked questions
Do I need to know Docker before learning Kubernetes? Yes — Kubernetes orchestrates Docker (or containerd) containers. Learn Docker first: build images, understand Dockerfiles and container networking. Then Kubernetes makes much more sense.
What's the difference between a Pod and a container? A container is a running process. A Pod is a K8s wrapper around one or more containers that share a network namespace and volumes. Most Pods run a single container — multi-container Pods are used for sidecar patterns (log shippers, proxies).
When should I use a StatefulSet instead of a Deployment? Use StatefulSets for stateful applications that need stable, persistent identities — databases (PostgreSQL, MySQL, Cassandra), message brokers (Kafka), and distributed systems where pod names and storage must be stable across restarts.
What is Helm and do I need it? Helm is a package manager for Kubernetes. Instead of managing many raw YAML files, you install and upgrade applications as versioned "charts". You don't need it to start, but it's essential for deploying production apps with configurable values across environments.
How is managed K8s (EKS/GKE/AKS) different from self-managed? Managed services run the control plane for you — you only manage worker nodes and workloads. This is cheaper to operate, removes upgrade complexity, and integrates with cloud services (IAM, load balancers, storage). Self-managed gives full control but requires significant ops expertise.
What is GitOps? GitOps means your Git repository is the single source of truth for your cluster state. Tools like ArgoCD or Flux watch your repo and automatically apply changes to the cluster when you push. This gives you full audit history, easy rollback, and consistent environments.