A Practical Introduction

Kubernetes
from pods
to production

Pods, deployments, services, ingress, storage, namespaces, and Helm — the full picture of container orchestration at scale.

pods deployments services ingress helm
press → or arrow-keys to advance
concept 01

When Docker Compose isn't enough

Compose runs on one host. Kubernetes spans a cluster of machines.

DOCKER COMPOSE Single Server · 1 host · 1 process Docker Engine nginx :80/:443 1 replica app :8000 1 replica postgres :5432 1 replica ⚠ Single Point of Failure Host goes down → all containers die No auto-restart · Scale by hand · No rolling updates KUBERNETES CLUSTER ⎈ Control Plane API Server · Scheduler · etcd · Controller Manager Node 1 nginx pod app pod kubelet · kube-proxy Node 2 app pod app pod kubelet · kube-proxy Node 3 postgres pod nginx pod kubelet · kube-proxy ✅ Node dies → pods reschedule automatically · kubectl scale --replicas=10
concept 02

Cluster Architecture

The control plane makes decisions. Worker nodes run your workloads.

kubectl CLI CONTROL PLANE kube-apiserver single entry point REST API · auth · validation etcd distributed key-value all cluster state kube-scheduler places pods on nodes resource-aware controller-manager reconciliation loops ReplicaSet · Node · Job… Worker Node 1 kubelet kube-proxy Pod Pod +room container runtime (containerd) Worker Node 2 kubelet kube-proxy Pod Pod Pod container runtime (containerd) Worker Node 3 kubelet kube-proxy Pod +room Pod container runtime (containerd)
concept 03

Pods — the smallest unit

A pod wraps one or more containers that share a network and storage namespace.

Pod (shared IP: 10.0.1.42) Shared network namespace · localhost · 1 IP Container: app python app.py listens on :8000 main app process Container: log-shipper tail /var/log/app.log forwards to Loki sidecar pattern localhost Shared Volume /var/log app writes logs · log-shipper reads logs emptyDir (ephemeral) or PersistentVolumeClaim pause container (invisible) holds the shared network namespace
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: myapp-pod
  labels:
    app: myapp
spec:
  containers:
  - name: app
    image: myapp:1.0
    ports:
    - containerPort: 8000
    resources:
      requests:
        cpu: "100m"
        memory: "128Mi"
      limits:
        cpu: "500m"
        memory: "256Mi"
  - name: log-shipper
    image: fluent-bit:3

Pod lifecycle

  • Pending — scheduler finding a node
  • Running — at least one container up
  • Succeeded — all containers exited 0
  • Failed — container exited non-zero

⚠ Pods are ephemeral

Never deploy a bare Pod in production. Pods don't self-heal. Use a Deployment — it recreates pods automatically.

concept 04

Deployments & Rolling Updates

A Deployment manages a ReplicaSet, which keeps N identical pods running at all times.

Deployment: myapp ReplicaSet (replicas: 3) BEFORE ROLLING UPDATE AFTER pod v1 Running pod v1 Running pod v1 Running pod v1 Running pod v1 Running pod v2 Starting… pod v2 Running pod v2 Running pod v2 Running Zero-downtime rollout · old pods stay up until new ones pass healthchecks kubectl set image deploy/myapp app=myapp:2.0
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: myapp:1.0
        ports:
        - containerPort: 8000

Key commands

# deploy / update
kubectl apply -f deploy.yaml
# watch rollout
kubectl rollout status deploy/myapp
# rollback
kubectl rollout undo deploy/myapp
concept 05

Services — stable virtual IPs

Pods come and go. A Service gives them a stable DNS name and load-balances across healthy pods.

ClusterIP internal only frontend Service 10.96.0.1:80 pod pod pod selector: app=backend ✅ reachable only inside cluster · DNS: backend.default.svc NodePort external via node IP client :30080 Node :30080 Service ClusterIP+ nodePort: 30080 → :80 inside ⚠ opens port on ALL nodes · only for dev/testing LoadBalancer cloud-provisioned LB Internet Cloud LB external IP Node1 Node2 type: LoadBalancer in Service spec ✅ cloud provisions LB automatically (AWS/GCP/Azure) How Services find Pods: label selectors Service selector: {app: myapp} → watches for Pods with that label → builds Endpoints list Pod starts / stops → Endpoints updates automatically → no config change needed kube-proxy on each node sets up iptables/IPVS rules → distributed load balancing built in DNS: <service-name>.<namespace>.svc.cluster.local
concept 06

Ingress — HTTP routing

One external LoadBalancer, many backend services. Routes traffic by hostname and path.

🌐 Internet HTTPS :443 🔒 LoadBalancer Service :443 published external IP Ingress Controller (nginx / traefik / haproxy) TLS termination 🔒 Ingress rules api.example.com → app.example.com → /static/* → CLUSTER INTERNAL api-service ClusterIP :3000 api pod api pod web-service ClusterIP :8080 web pod web pod cdn-service /static/* → :9000 Ingress YAML kind: Ingress rules: - host: api.example.com backend: service: api-service port: 3000 - host: app.example.com backend: service: web-service port: 8080 tls: [{hosts:[...]}]
concept 07

ConfigMaps & Secrets

Decouple configuration from your container image. Never bake credentials into an image.

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  MAX_CONNECTIONS: "100"
  app.conf: |
    server.port=8000
    cache.ttl=300

# secret.yaml  (values are base64-encoded)
apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:   # kubectl encodes for you
  DATABASE_URL: "postgres://user:pass@db/mydb"
  API_KEY: "s3cr3t-k3y-here"

# pod consuming both
spec:
  containers:
  - name: app
    envFrom:
    - configMapRef:
        name: app-config
    - secretRef:
        name: app-secrets
    volumeMounts:
    - name: config-vol
      mountPath: /etc/app
  volumes:
  - name: config-vol
    configMap:
      name: app-config
Pod (app container) env: LOG_LEVEL=info env: DATABASE_URL=… /etc/app/app.conf ConfigMap app-config Secret app-secrets

ConfigMap vs Secret

  • ConfigMap — non-sensitive config, plain text
  • Secret — passwords, API keys, certs (base64)
  • Both can be env vars or volume mounts
  • Changes to ConfigMaps auto-refresh mounted files

⚠ Secrets are not encrypted

Base64 is encoding, not encryption. Use Sealed Secrets, Vault, or an external secrets operator for production.

concept 08

Persistent Volumes

Storage in Kubernetes is decoupled from pods via a claim-and-bind model.

StorageClass fast-ssd provisioner: ebs.csi type: gp3 · 3000 IOPS ① dynamic provision PersistentVolume pv-database-01 capacity: 20Gi accessMode: ReadWriteOnce reclaimPolicy: Retain SSD disk ② bound to PVC PersistentVolumeClaim postgres-data requests: 20Gi Status: Bound ✅ storageClass: fast-ssd ③ mounted in Pod Pod (postgres) image: postgres:16 volumeMount: /var/lib/postgresql claimName: postgres-data Pod restarts → data survives ✅ PVCs outlive pods · delete pod → data safe · delete PVC → depends on reclaimPolicy (Retain keeps the disk)
concept 09

Namespaces — logical isolation

Virtual clusters within a cluster. Divide teams, environments, or tenants without separate clusters.

Kubernetes Cluster ns: production deploy/app 3 replicas svc/app ClusterIP pvc/data 20Gi secret/db-creds ns: staging deploy/app 1 replica svc/app ClusterIP pvc/data 5Gi secret/db-creds ns: monitoring prometheus grafana ResourceQuota: 4cpu / 8Gi RAM ns: kube-system coredns kube-proxy system components · don't touch Services in different namespaces don't conflict · cross-ns: svc.namespace.svc.cluster.local

kubectl with namespaces

# create namespace
kubectl create ns production
# deploy into a namespace
kubectl apply -f app.yaml -n production
# list pods in namespace
kubectl get pods -n production
# switch default namespace
kubectl config set-context --current \
  --namespace=production
# all namespaces at once
kubectl get pods -A

ResourceQuota

kind: ResourceQuota
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    pods: "20"

Built-in namespaces

  • default — where you land without -n
  • kube-system — system components
  • kube-public — readable by all
concept 10

Helm — the package manager

Charts are parameterised YAML templates. One chart, many environments, one command to deploy.

# Chart structure
mychart/
  Chart.yaml          # name, version, description
  values.yaml         # default values (override these)
  templates/
    deployment.yaml   # uses {{ .Values.* }}
    service.yaml
    ingress.yaml
    _helpers.tpl      # named templates

# values.yaml (defaults)
replicaCount: 1
image:
  repository: myapp
  tag: "1.0"
service:
  port: 8000
ingress:
  enabled: true
  host: app.example.com
resources:
  limits:
    cpu: 500m
    memory: 256Mi

# template snippet (deployment.yaml)
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
      - image: {{ .Values.image.repository }}:
            {{- .Values.image.tag }}

Essential Helm commands

# install from chart repo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install mydb bitnami/postgresql

# install with custom values
helm install myapp ./mychart \
  --namespace prod \
  -f prod-values.yaml

# upgrade (rolling update)
helm upgrade myapp ./mychart --set image.tag=2.0

# rollback
helm rollback myapp 1

# list installed releases
helm list -n prod

Why Helm?

  • One chart → dev, staging, prod with different values
  • Atomic install/upgrade/rollback as a unit
  • Artifact Hub: thousands of community charts
  • Release history tracked by Helm

Helm 3 vs Helm 2

Helm 3 removed Tiller (the server component). No special cluster permissions needed. Just brew install helm and go.

concept 11

kubectl cheatsheet

The essential commands for day-to-day cluster work.

# ── Apply & Delete ──────────────────
kubectl apply -f manifest.yaml
kubectl delete -f manifest.yaml
kubectl delete pod mypod --grace-period=0

# ── Inspect ─────────────────────────
kubectl get pods -n prod -o wide
kubectl get all -n prod
kubectl describe pod <name>
kubectl describe node <name>

# ── Logs & Exec ─────────────────────
kubectl logs -f deploy/myapp
kubectl logs mypod -c sidecar    # specific container
kubectl exec -it mypod -- /bin/sh

# ── Port Forwarding ─────────────────
kubectl port-forward svc/myapp 8080:80
kubectl port-forward pod/mypod 5432:5432

# ── Scaling ─────────────────────────
kubectl scale deploy/myapp --replicas=5
kubectl autoscale deploy/myapp \
  --min=2 --max=10 --cpu-percent=70
# ── Rollouts ────────────────────────
kubectl set image deploy/myapp app=myapp:2.0
kubectl rollout status deploy/myapp
kubectl rollout history deploy/myapp
kubectl rollout undo deploy/myapp
kubectl rollout undo deploy/myapp --to-revision=2

# ── Config & Secrets ────────────────
kubectl create configmap app-cfg \
  --from-file=config.json
kubectl create secret generic mysecret \
  --from-literal=password=supersecret

# ── Context & Namespace ─────────────
kubectl config get-contexts
kubectl config use-context my-cluster
kubectl config set-context --current \
  --namespace=production

# ── Useful flags ────────────────────
-n <ns>          namespace
-o yaml          full YAML output
-o jsonpath=…    extract a field
--dry-run=client validate without applying
-l app=myapp     filter by label
recap

You now know Kubernetes

The complete picture — from single pods to production-grade orchestration.

⎈ Core Objects

  • Pod — group of containers, 1 IP, shared volumes
  • Deployment — desired-state pods + rolling updates
  • ReplicaSet — ensures N pods running at all times
  • Namespace — logical isolation within a cluster

🌐 Networking

  • Service (ClusterIP) — stable DNS inside cluster
  • Service (NodePort/LB) — external exposure
  • Ingress — HTTP routing, TLS termination, path/host rules
  • kube-proxy on each node implements iptables rules

💾 Storage & Config

  • ConfigMap — non-sensitive config as env/files
  • Secret — credentials (use Vault/Sealed Secrets)
  • PVC/PV — claim-and-bind storage model
  • StorageClass — dynamic provisioning from cloud

⎈ Helm

Package manager for Kubernetes. One chart → parameterised YAML → helm install myapp ./chart -f prod-values.yaml. Atomic upgrade + rollback built in.

⚡ Key mental model

You declare the desired state in YAML. The control plane reconciles reality to match it — forever. Nodes fail, pods die, the scheduler replaces them. You never SSH in.

Next steps: kubernetes.io/docs · killercoda.com · helm.sh/docs · k3s.io (lightweight local cluster)