What is Kubernetes and Why Should You Care?

Kubernetes (often abbreviated as K8s) is an open-source container orchestration platform originally developed by Google. It automates the deployment, scaling, and management of containerized applications. If you have ever struggled with deploying Docker containers across multiple servers, managing service discovery, or handling rolling updates without downtime, Kubernetes is the answer.

In production environments, running a single Docker container on one server simply does not scale. You need a system that can distribute containers across multiple machines, restart them when they crash, scale them up during traffic spikes, and roll out updates seamlessly. That is exactly what Kubernetes does.

Key Benefits of Kubernetes

  • Self-healing: Automatically restarts failed containers and replaces unhealthy ones
  • Horizontal scaling: Scale applications up or down based on CPU, memory, or custom metrics
  • Service discovery and load balancing: Automatically exposes containers using DNS or IP addresses
  • Automated rollouts and rollbacks: Deploy new versions with zero downtime
  • Secret and configuration management: Store sensitive data separately from application code
  • Storage orchestration: Automatically mount storage systems like local, cloud, or network storage

Kubernetes Architecture Deep Dive

Understanding the architecture is crucial before working with Kubernetes. A K8s cluster consists of two main components: the Control Plane and the Worker Nodes.

Control Plane Components

The control plane is the brain of the cluster. It makes global decisions about the cluster, detects and responds to cluster events, and schedules workloads.

  • kube-apiserver: The front-end for the Kubernetes control plane. All communication goes through this REST API server. When you run kubectl commands, they talk to this component.
  • etcd: A consistent and highly-available key-value store used as the backing store for all cluster data. Every piece of information about your cluster state is stored here.
  • kube-scheduler: Watches for newly created Pods with no assigned node and selects the best node for them to run on based on resource requirements, affinity rules, and constraints.
  • kube-controller-manager: Runs controller processes including the Node Controller (notices when nodes go down), Replication Controller (maintains correct pod count), and Endpoints Controller.
  • cloud-controller-manager: Links your cluster to your cloud provider's API for managing load balancers, storage volumes, and routing.

Worker Node Components

  • kubelet: An agent that runs on each worker node. It ensures containers described in PodSpecs are running and healthy. The kubelet communicates with the API server to receive pod assignments.
  • kube-proxy: Maintains network rules on nodes, allowing network communication to your Pods from inside or outside the cluster. It implements the Kubernetes Service concept.
  • Container Runtime: The software responsible for running containers. Kubernetes supports containerd, CRI-O, and any implementation of the Kubernetes Container Runtime Interface (CRI).

Installing Minikube and kubectl

Minikube runs a single-node Kubernetes cluster on your local machine for development and testing. Let us set up a complete local environment.

Install kubectl (Kubernetes CLI)

# macOS
brew install kubectl

# Windows (using Chocolatey)
choco install kubernetes-cli

# Linux (Ubuntu/Debian)
curl -LO "https://dl.k8s.io/release/$(curl -L -s 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 installation
kubectl version --client

Install Minikube

# macOS
brew install minikube

# Windows (using Chocolatey)
choco 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 the cluster
minikube start --driver=docker --memory=4096 --cpus=2

# Verify the cluster is running
kubectl cluster-info
kubectl get nodes

You should see output confirming your cluster is running with one node in the Ready state.

Core Kubernetes Resources Explained

1. Pods — The Smallest Deployable Unit

A Pod represents one or more containers that share storage, network, and a specification for how to run. Pods are ephemeral — they can be created, destroyed, and replaced at any time.

# pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-nginx-pod
  labels:
    app: nginx
    environment: development
spec:
  containers:
  - name: nginx
    image: nginx:1.25-alpine
    ports:
    - containerPort: 80
    resources:
      requests:
        memory: "64Mi"
        cpu: "250m"
      limits:
        memory: "128Mi"
        cpu: "500m"
    livenessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 10
      periodSeconds: 5
    readinessProbe:
      httpGet:
        path: /
        port: 80
      initialDelaySeconds: 5
      periodSeconds: 3
# Create the pod
kubectl apply -f pod.yaml

# Check pod status
kubectl get pods

# Describe pod details
kubectl describe pod my-nginx-pod

# View pod logs
kubectl logs my-nginx-pod

# Execute a command inside the pod
kubectl exec -it my-nginx-pod -- /bin/sh

2. Deployments — Managing Pod Replicas

Deployments manage ReplicaSets and provide declarative updates for Pods. They handle rolling updates, rollbacks, and scaling. You should almost never create Pods directly; always use Deployments.

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs-app
  labels:
    app: nodejs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nodejs-app
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: nodejs-app
        version: "1.0.0"
    spec:
      containers:
      - name: nodejs
        image: myregistry/nodejs-app:1.0.0
        ports:
        - containerPort: 3000
        env:
        - name: NODE_ENV
          value: "production"
        - name: PORT
          value: "3000"
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 15
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
# Apply the deployment
kubectl apply -f deployment.yaml

# Watch the rollout
kubectl rollout status deployment/nodejs-app

# View deployment details
kubectl get deployments
kubectl describe deployment nodejs-app

3. Services — Exposing Your Application

Services provide a stable network endpoint to access your Pods. Since Pods are ephemeral and their IP addresses change, Services give you a consistent way to reach your application.

# service-clusterip.yaml (internal access only)
apiVersion: v1
kind: Service
metadata:
  name: nodejs-service
spec:
  type: ClusterIP
  selector:
    app: nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000

---
# service-nodeport.yaml (external access via node IP)
apiVersion: v1
kind: Service
metadata:
  name: nodejs-nodeport
spec:
  type: NodePort
  selector:
    app: nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000
    nodePort: 30080

---
# service-loadbalancer.yaml (cloud load balancer)
apiVersion: v1
kind: Service
metadata:
  name: nodejs-lb
spec:
  type: LoadBalancer
  selector:
    app: nodejs-app
  ports:
  - protocol: TCP
    port: 80
    targetPort: 3000

4. ConfigMaps — Externalized Configuration

# configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  DATABASE_HOST: "postgres-service"
  DATABASE_PORT: "5432"
  LOG_LEVEL: "info"
  APP_NAME: "My Node.js App"
  nginx.conf: |
    server {
      listen 80;
      location / {
        proxy_pass http://localhost:3000;
      }
    }
# Using ConfigMap in a Deployment
spec:
  containers:
  - name: app
    image: myapp:1.0
    envFrom:
    - configMapRef:
        name: app-config
    # Or mount as a file
    volumeMounts:
    - name: config-volume
      mountPath: /etc/nginx/conf.d
  volumes:
  - name: config-volume
    configMap:
      name: app-config
      items:
      - key: nginx.conf
        path: default.conf

5. Secrets — Sensitive Data Management

# Create a secret from literals
kubectl create secret generic db-credentials 
  --from-literal=username=admin 
  --from-literal=password=s3cur3P@ssw0rd

# Create a secret from a file
kubectl create secret generic tls-cert 
  --from-file=cert.pem=./server.crt 
  --from-file=key.pem=./server.key
# secret.yaml (values must be base64-encoded)
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=
  password: czNjdXIzUEBzc3cwcmQ=

---
# Using Secrets in pods
spec:
  containers:
  - name: app
    image: myapp:1.0
    env:
    - name: DB_USERNAME
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: username
    - name: DB_PASSWORD
      valueFrom:
        secretKeyRef:
          name: db-credentials
          key: password

6. PersistentVolumes and PersistentVolumeClaims

# persistentvolume.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: app-pv
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: standard
  hostPath:
    path: /data/app-storage

---
# persistentvolumeclaim.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard

---
# Using PVC in a Deployment
spec:
  containers:
  - name: app
    volumeMounts:
    - name: app-storage
      mountPath: /app/data
  volumes:
  - name: app-storage
    persistentVolumeClaim:
      claimName: app-pvc

7. Ingress — HTTP Routing and TLS

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - myapp.example.com
    secretName: myapp-tls
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: nodejs-service
            port:
              number: 80
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 8080
# Install NGINX Ingress Controller on Minikube
minikube addons enable ingress

# Verify ingress controller
kubectl get pods -n ingress-nginx

Deploying a Real Node.js App to Kubernetes Step-by-Step

Let us build and deploy a complete Node.js application to Kubernetes from scratch.

Step 1: Create the Node.js Application

// server.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.get('/', (req, res) => {
  res.json({
    message: 'Hello from Kubernetes!',
    hostname: require('os').hostname(),
    version: process.env.APP_VERSION || '1.0.0',
    timestamp: new Date().toISOString()
  });
});

app.get('/health', (req, res) => {
  res.status(200).json({ status: 'healthy' });
});

app.get('/ready', (req, res) => {
  res.status(200).json({ status: 'ready' });
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Step 2: Create the Dockerfile

# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:20-alpine
WORKDIR /app
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]

Step 3: Build and Push the Image

# For Minikube, use the local Docker daemon
eval $(minikube docker-env)

# Build the image
docker build -t nodejs-k8s-demo:1.0.0 .

# For a real registry
docker build -t yourusername/nodejs-k8s-demo:1.0.0 .
docker push yourusername/nodejs-k8s-demo:1.0.0

Step 4: Create Kubernetes Manifests

# k8s/namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: demo-app

---
# k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: demo-app
data:
  APP_VERSION: "1.0.0"
  NODE_ENV: "production"

---
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nodejs-app
  namespace: demo-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nodejs-app
  template:
    metadata:
      labels:
        app: nodejs-app
    spec:
      containers:
      - name: nodejs
        image: nodejs-k8s-demo:1.0.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 3000
        envFrom:
        - configMapRef:
            name: app-config
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "250m"
            memory: "256Mi"

---
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nodejs-service
  namespace: demo-app
spec:
  type: NodePort
  selector:
    app: nodejs-app
  ports:
  - port: 80
    targetPort: 3000
    nodePort: 30080

Step 5: Deploy Everything

# Apply all manifests
kubectl apply -f k8s/

# Check status
kubectl get all -n demo-app

# Access the application
minikube service nodejs-service -n demo-app --url

Scaling with Horizontal Pod Autoscaler (HPA)

HPA automatically scales the number of Pods based on observed CPU utilization, memory usage, or custom metrics.

# Enable metrics-server on Minikube
minikube addons enable metrics-server

# Create an HPA via command line
kubectl autoscale deployment nodejs-app -n demo-app 
  --cpu-percent=50 
  --min=2 
  --max=10
# hpa.yaml — declarative HPA with multiple metrics
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: nodejs-hpa
  namespace: demo-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: nodejs-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 50
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 70
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
    scaleUp:
      stabilizationWindowSeconds: 60
# Watch HPA in action
kubectl get hpa -n demo-app --watch

# Load test to trigger scaling
kubectl run load-generator --image=busybox --rm -it -- 
  /bin/sh -c "while true; do wget -q -O- http://nodejs-service.demo-app.svc.cluster.local; done"

Rolling Updates and Rollbacks

Performing a Rolling Update

# Update the image version
kubectl set image deployment/nodejs-app 
  nodejs=nodejs-k8s-demo:2.0.0 -n demo-app

# Watch the rollout progress
kubectl rollout status deployment/nodejs-app -n demo-app

# View rollout history
kubectl rollout history deployment/nodejs-app -n demo-app

Rolling Back a Bad Deployment

# Rollback to previous version
kubectl rollout undo deployment/nodejs-app -n demo-app

# Rollback to a specific revision
kubectl rollout undo deployment/nodejs-app --to-revision=2 -n demo-app

# Pause and resume rollouts
kubectl rollout pause deployment/nodejs-app -n demo-app
kubectl rollout resume deployment/nodejs-app -n demo-app

Namespaces — Organizing Your Cluster

# List namespaces
kubectl get namespaces

# Create a namespace
kubectl create namespace staging

# Set default namespace for context
kubectl config set-context --current --namespace=demo-app

# Run commands in a specific namespace
kubectl get pods -n kube-system
kubectl get all --all-namespaces

Resource Limits and Requests

Resource requests guarantee a minimum amount of CPU and memory for your pods. Limits set the maximum. Always set both to prevent resource starvation and cluster instability.

# LimitRange for namespace defaults
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: demo-app
spec:
  limits:
  - default:
      cpu: "500m"
      memory: "256Mi"
    defaultRequest:
      cpu: "100m"
      memory: "128Mi"
    type: Container

---
# ResourceQuota for namespace totals
apiVersion: v1
kind: ResourceQuota
metadata:
  name: namespace-quota
  namespace: demo-app
spec:
  hard:
    requests.cpu: "4"
    requests.memory: "8Gi"
    limits.cpu: "8"
    limits.memory: "16Gi"
    pods: "20"

Helm Charts Introduction

Helm is the package manager for Kubernetes. It lets you define, install, and upgrade complex Kubernetes applications using charts — a collection of templated YAML files.

Installing Helm

# macOS
brew install helm

# Windows
choco install kubernetes-helm

# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Verify
helm version

Creating a Helm Chart

# Scaffold a new chart
helm create nodejs-chart

# Chart structure:
# nodejs-chart/
#   Chart.yaml          - Chart metadata
#   values.yaml         - Default configuration values
#   templates/          - Template files
#     deployment.yaml
#     service.yaml
#     ingress.yaml
#     _helpers.tpl      - Template helpers
#   charts/             - Sub-chart dependencies
# values.yaml
replicaCount: 3

image:
  repository: myregistry/nodejs-app
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  host: myapp.example.com

resources:
  limits:
    cpu: 500m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilization: 50
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "nodejs-chart.fullname" . }}
  labels:
    {{- include "nodejs-chart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "nodejs-chart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "nodejs-chart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: 3000
        resources:
          {{- toYaml .Values.resources | nindent 12 }}
# Install the chart
helm install my-release ./nodejs-chart

# Install with custom values
helm install my-release ./nodejs-chart 
  --set replicaCount=5 
  --set image.tag="2.0.0"

# Upgrade
helm upgrade my-release ./nodejs-chart -f production-values.yaml

# Rollback
helm rollback my-release 1

# List releases
helm list

# Uninstall
helm uninstall my-release

Monitoring with Prometheus and Grafana

# Install using Helm
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Install the kube-prometheus-stack (includes Prometheus, Grafana, and Alertmanager)
helm install monitoring prometheus-community/kube-prometheus-stack 
  --namespace monitoring 
  --create-namespace 
  --set grafana.adminPassword=admin123

# Access Grafana dashboard
kubectl port-forward svc/monitoring-grafana 3000:80 -n monitoring
# Open http://localhost:3000 — username: admin, password: admin123

# Access Prometheus UI
kubectl port-forward svc/monitoring-kube-prometheus-prometheus 9090:9090 -n monitoring

Debugging Pods — Troubleshooting Guide

Problem: CrashLoopBackOff

Cause: The container starts and immediately crashes, repeatedly. Common reasons include application errors, missing environment variables, or incorrect commands.

Solution:

# Check the logs from the crashed container
kubectl logs pod-name --previous

# Describe the pod for events
kubectl describe pod pod-name

# Start an interactive debugging session
kubectl run debug --image=busybox -it --rm -- /bin/sh

# Common fixes:
# 1. Check your CMD/ENTRYPOINT in Dockerfile
# 2. Verify all required env vars are set
# 3. Ensure the app binds to 0.0.0.0, not 127.0.0.1

Problem: ImagePullBackOff

Cause: Kubernetes cannot pull the container image. The image name may be wrong, the registry may require authentication, or the tag does not exist.

Solution:

# Check the exact error
kubectl describe pod pod-name | grep -A 5 "Events"

# Create a registry secret
kubectl create secret docker-registry regcred 
  --docker-server=https://index.docker.io/v1/ 
  --docker-username=yourusername 
  --docker-password=yourpassword 
  --docker-email=you@example.com

# Reference in your deployment
# spec.template.spec.imagePullSecrets:
#   - name: regcred

Problem: OOMKilled

Cause: The container exceeded its memory limit and was killed by the kernel.

Solution:

# Check the termination reason
kubectl describe pod pod-name | grep -A 3 "Last State"

# Increase memory limits in your deployment
# resources:
#   limits:
#     memory: "512Mi"  # Increase from 256Mi

# Profile your app memory usage
kubectl top pods -n demo-app

Problem: Pending Pods

Cause: No node has enough resources to schedule the Pod, or there are node affinity/taint issues.

Solution:

# Check why the pod is pending
kubectl describe pod pod-name | grep -A 10 "Events"

# Check node resources
kubectl describe nodes | grep -A 5 "Allocated resources"

# Check resource quotas
kubectl get resourcequota -n demo-app

Common kubectl Commands Cheat Sheet

CommandDescription
kubectl get pods -AList all pods in all namespaces
kubectl get pods -o wideShow pods with node and IP info
kubectl logs pod-name -fStream logs in real time
kubectl logs pod-name -c containerLogs from a specific container
kubectl exec -it pod-name -- bashOpen a shell inside a pod
kubectl describe pod pod-nameDetailed pod information and events
kubectl get events --sort-by=.metadata.creationTimestampCluster events sorted by time
kubectl top nodesNode resource usage
kubectl top podsPod resource usage
kubectl port-forward svc/name 8080:80Forward a local port to a service
kubectl cp file.txt pod-name:/tmp/Copy files to/from a pod
kubectl delete pod pod-name --forceForce delete a stuck pod
kubectl scale deployment name --replicas=5Manually scale a deployment
kubectl edit deployment nameEdit a resource in your editor
kubectl apply -f . --dry-run=clientValidate manifests without applying
kubectl diff -f deployment.yamlPreview changes before applying
kubectl get pod -o yaml > pod.yamlExport resource definition
kubectl api-resourcesList all available resource types
kubectl config get-contextsList all configured contexts
kubectl config use-context nameSwitch between clusters

Best Practices Summary

  1. Always use Deployments, never create raw Pods
  2. Set resource requests and limits on every container
  3. Use namespaces to isolate environments (dev, staging, prod)
  4. Store configuration in ConfigMaps and sensitive data in Secrets
  5. Use liveness and readiness probes for all production workloads
  6. Implement Horizontal Pod Autoscaler for variable workloads
  7. Use Helm charts for repeatable, version-controlled deployments
  8. Tag images with specific versions, never use latest in production
  9. Set up monitoring and alerting with Prometheus and Grafana
  10. Practice rollbacks regularly so you are ready when things go wrong