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
kubectlcommands, 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
| Command | Description |
|---|---|
kubectl get pods -A | List all pods in all namespaces |
kubectl get pods -o wide | Show pods with node and IP info |
kubectl logs pod-name -f | Stream logs in real time |
kubectl logs pod-name -c container | Logs from a specific container |
kubectl exec -it pod-name -- bash | Open a shell inside a pod |
kubectl describe pod pod-name | Detailed pod information and events |
kubectl get events --sort-by=.metadata.creationTimestamp | Cluster events sorted by time |
kubectl top nodes | Node resource usage |
kubectl top pods | Pod resource usage |
kubectl port-forward svc/name 8080:80 | Forward a local port to a service |
kubectl cp file.txt pod-name:/tmp/ | Copy files to/from a pod |
kubectl delete pod pod-name --force | Force delete a stuck pod |
kubectl scale deployment name --replicas=5 | Manually scale a deployment |
kubectl edit deployment name | Edit a resource in your editor |
kubectl apply -f . --dry-run=client | Validate manifests without applying |
kubectl diff -f deployment.yaml | Preview changes before applying |
kubectl get pod -o yaml > pod.yaml | Export resource definition |
kubectl api-resources | List all available resource types |
kubectl config get-contexts | List all configured contexts |
kubectl config use-context name | Switch between clusters |
Best Practices Summary
- Always use Deployments, never create raw Pods
- Set resource requests and limits on every container
- Use namespaces to isolate environments (dev, staging, prod)
- Store configuration in ConfigMaps and sensitive data in Secrets
- Use liveness and readiness probes for all production workloads
- Implement Horizontal Pod Autoscaler for variable workloads
- Use Helm charts for repeatable, version-controlled deployments
- Tag images with specific versions, never use
latestin production - Set up monitoring and alerting with Prometheus and Grafana
- Practice rollbacks regularly so you are ready when things go wrong