Toolmingo
Guides21 min read

Kubernetes Tutorial for Beginners (2025): Learn K8s Step by Step

Complete Kubernetes tutorial for beginners. Learn Pods, Deployments, Services, ConfigMaps, scaling, and deploy real apps to K8s. Free guide with examples.

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.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools