What Is Kubernetes?
Kubernetes (often abbreviated K8s) is an open-source container orchestration platform originally developed by Google and now maintained by the Cloud Native Computing Foundation (CNCF). It automates deploying, scaling, and managing containerized applications across clusters of machines.
Where Docker builds and runs individual containers, Kubernetes answers the harder questions: How do you run 50 copies of your application across 10 servers? How do you roll out updates without downtime? What happens when a server goes down? How do you route traffic to healthy instances?
Core Architecture
A Kubernetes cluster consists of a control plane (the brain) and worker nodes (the machines that run your containers). Here is what each component does:
| Component | Location | Role |
|---|---|---|
kube-apiserver |
Control Plane | Front door for all API requests. kubectl talks to this. |
etcd |
Control Plane | Key-value store holding all cluster state. |
kube-scheduler |
Control Plane | Decides which node runs each new Pod. |
controller-manager |
Control Plane | Ensures desired state matches actual state (runs controllers). |
kubelet |
Worker Node | Agent that runs Pods on each node. |
kube-proxy |
Worker Node | Handles network routing and load balancing. |
You interact with Kubernetes through kubectl, the command-line tool that sends requests to the API server. Everything in Kubernetes is declared as YAML manifests: you describe the desired state, and Kubernetes works to make it real.
Setting Up Your Environment
You need two things to follow this tutorial: kubectl (the CLI) and a local cluster. The fastest way to get a local cluster running in 2026:
Option 1: Docker Desktop (Simplest)
# Docker Desktop includes a built-in Kubernetes cluster
# Settings > Kubernetes > Enable Kubernetes > Apply & Restart
# Verify it is running
kubectl cluster-info
kubectl get nodes
Option 2: Minikube (Most Popular)
# Install minikube (macOS)
brew install minikube
# Start a cluster
minikube start
# Verify
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# minikube Ready control-plane 1m v1.31.0
# Enable useful addons
minikube addons enable ingress
minikube addons enable metrics-server
minikube addons enable dashboard
# Open the dashboard
minikube dashboard
Option 3: kind (Kubernetes in Docker)
# Install kind
brew install kind
# Create a cluster
kind create cluster --name dev
# Verify
kubectl cluster-info --context kind-dev
Docker Desktop for the simplest setup. Minikube for the most features (add-ons, multi-node). kind for CI/CD pipelines and when you need multiple clusters. All three are free and run locally.
Pods: The Smallest Unit
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share the same network namespace (they communicate via localhost) and storage volumes. In practice, most Pods run a single container.
Creating Your First Pod
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: "250m"
# Create the Pod
kubectl apply -f pod.yaml
# Check Pod status
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# nginx-pod 1/1 Running 0 15s
# Detailed Pod information
kubectl describe pod nginx-pod
# View Pod logs
kubectl logs nginx-pod
# Follow logs in real time
kubectl logs -f nginx-pod
# Open a shell inside the Pod
kubectl exec -it nginx-pod -- /bin/sh
# Port-forward to access from localhost
kubectl port-forward nginx-pod 8080:80
# Visit http://localhost:8080
# Delete the Pod
kubectl delete pod nginx-pod
Bare Pods are not rescheduled if the node fails. Always use a Deployment (or StatefulSet, DaemonSet, Job) to manage Pods. These controllers ensure your Pods are recreated if they crash or a node goes down.
Deployments: Managing Replicas
A Deployment is the standard way to run stateless applications. It manages a set of identical Pods (called replicas), handles rolling updates, and automatically replaces Pods that crash or get evicted. You describe the desired state, and the Deployment controller makes it happen.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
labels:
app: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: myregistry.com/web-app:v1.0.0
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 15
periodSeconds: 20
# Apply the Deployment
kubectl apply -f deployment.yaml
# Check rollout status
kubectl rollout status deployment/web-app
# List Deployments
kubectl get deployments
# See the Pods created by the Deployment
kubectl get pods -l app=web-app
# Scale to 5 replicas
kubectl scale deployment web-app --replicas=5
# Update the image (triggers rolling update)
kubectl set image deployment/web-app web=myregistry.com/web-app:v1.1.0
# Watch the rolling update progress
kubectl rollout status deployment/web-app
# View rollout history
kubectl rollout history deployment/web-app
# Rollback to previous version
kubectl rollout undo deployment/web-app
# Rollback to a specific revision
kubectl rollout undo deployment/web-app --to-revision=2
The RollingUpdate strategy with maxUnavailable: 0 ensures zero downtime during deployments: Kubernetes creates new Pods before removing old ones. The readiness probe controls when a Pod receives traffic, preventing requests from reaching containers that are not yet ready.
Services: Networking and Discovery
Pods are ephemeral. They get new IP addresses every time they restart. A Service provides a stable network identity and load balances traffic across a set of Pods. Services use label selectors to find their target Pods.
Service Types
| Type | Access Scope | Use Case |
|---|---|---|
ClusterIP |
Internal only | Service-to-service communication within the cluster |
NodePort |
External via node IP + port | Development, testing |
LoadBalancer |
External via cloud LB | Production external access (AWS ALB, GCP LB) |
ExternalName |
DNS alias | Point to external services (e.g., RDS database) |
# Internal service (ClusterIP - default)
apiVersion: v1
kind: Service
metadata:
name: web-app-internal
spec:
selector:
app: web-app
ports:
- port: 80
targetPort: 3000
protocol: TCP
type: ClusterIP
---
# External service (LoadBalancer)
apiVersion: v1
kind: Service
metadata:
name: web-app-public
spec:
selector:
app: web-app
ports:
- port: 80
targetPort: 3000
protocol: TCP
type: LoadBalancer
Ingress: HTTP Routing
An Ingress resource provides HTTP/HTTPS routing with path-based and host-based rules. Instead of one LoadBalancer per service (expensive), a single Ingress controller routes traffic to many services.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
ingressClassName: nginx
tls:
- hosts:
- app.example.com
- api.example.com
secretName: app-tls
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-app-internal
port:
number: 80
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80
When generating Nginx configurations for your Ingress controller, the Nginx Config Generator helps you create custom server blocks with SSL, rate limiting, and caching directives.
ConfigMaps and Secrets
ConfigMaps store non-sensitive configuration data. Secrets store sensitive data like passwords, API keys, and TLS certificates. Both decouple configuration from container images so you can use the same image across development, staging, and production.
ConfigMaps
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: "production"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
config.json: |
{
"features": {
"darkMode": true,
"betaAccess": false
},
"pagination": {
"defaultLimit": 25,
"maxLimit": 100
}
}
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: myregistry.com/web-app:v1.0.0
# Inject as environment variables
envFrom:
- configMapRef:
name: app-config
# Or mount as files
volumeMounts:
- name: config-volume
mountPath: /app/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: config.json
path: config.json
Secrets
# Create from literal values
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password='s3cur3-p@ssw0rd'
# Create from a file
kubectl create secret generic tls-cert \
--from-file=cert.pem=./certs/server.crt \
--from-file=key.pem=./certs/server.key
# Create from .env file
kubectl create secret generic app-secrets \
--from-env-file=.env.production
# View secret (base64 encoded)
kubectl get secret db-credentials -o yaml
# Decode a specific value
kubectl get secret db-credentials -o jsonpath='{.data.password}' | base64 --decode
When you need to encode or decode base64 values for Kubernetes secrets, the Base64 Encoder/Decoder handles encoding in both directions without installing any tools.
Kubernetes Secrets are base64-encoded, not encrypted. Anyone with kubectl access can decode them. In production, enable encryption at rest for etcd, use a secrets manager (Vault, AWS Secrets Manager, GCP Secret Manager), and restrict access with RBAC.
Scaling: Manual and Automatic
Kubernetes supports three types of scaling: horizontal (more Pods), vertical (bigger Pods), and cluster-level (more nodes).
Manual Scaling
# Scale to 5 replicas
kubectl scale deployment web-app --replicas=5
# Scale to 0 (useful for cost savings on non-prod)
kubectl scale deployment web-app --replicas=0
# Verify
kubectl get deployment web-app
# NAME READY UP-TO-DATE AVAILABLE AGE
# web-app 5/5 5 5 10m
Horizontal Pod Autoscaler (HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Pods
value: 1
periodSeconds: 60
# Apply the HPA
kubectl apply -f hpa.yaml
# Watch HPA in action
kubectl get hpa --watch
# NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS
# web-app-hpa Deployment/web-app 45%/70% 2 10 3
# Quick HPA creation from CLI
kubectl autoscale deployment web-app \
--min=2 --max=10 --cpu-percent=70
The stabilizationWindowSeconds prevents flapping: the HPA waits 5 minutes before scaling down, so brief traffic dips do not cause unnecessary churn.
Persistent Storage
Pods are ephemeral, but many applications need data that survives Pod restarts. PersistentVolumes (PV) and PersistentVolumeClaims (PVC) provide durable storage.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-data
spec:
accessModes:
- ReadWriteOnce
storageClassName: standard
resources:
requests:
storage: 10Gi
---
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
ports:
- containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgres-data
Use a StatefulSet instead of a Deployment for databases and other stateful applications. StatefulSets provide stable network identities (postgres-0, postgres-1), ordered deployment and scaling, and stable persistent storage per replica.
Debugging and Troubleshooting
When things break in Kubernetes, systematic debugging is essential. Here are the commands and techniques that solve 90% of issues.
Essential kubectl Debug Commands
# Check Pod status and events
kubectl get pods -o wide
kubectl describe pod <pod-name>
# View logs (current container)
kubectl logs <pod-name>
# View logs from previous crashed container
kubectl logs <pod-name> --previous
# View logs from a specific container in a multi-container Pod
kubectl logs <pod-name> -c <container-name>
# Follow logs in real time
kubectl logs -f <pod-name>
# Stream logs from all Pods with a label
kubectl logs -l app=web-app --all-containers -f
# Open a shell in a running Pod
kubectl exec -it <pod-name> -- /bin/sh
# Run a temporary debug Pod on the cluster
kubectl run debug --rm -it --image=busybox -- /bin/sh
# Check resource usage (requires metrics-server)
kubectl top pods
kubectl top nodes
# View cluster events (sorted by time)
kubectl get events --sort-by='.lastTimestamp'
# Check node conditions
kubectl describe node <node-name>
# View all resources in a namespace
kubectl get all -n <namespace>
Common Pod Status Problems
| Status | Meaning | First Debug Step |
|---|---|---|
Pending |
Cannot be scheduled | kubectl describe pod — check Events for resource/node issues |
CrashLoopBackOff |
Container keeps crashing | kubectl logs --previous — see crash output |
ImagePullBackOff |
Cannot pull container image | Check image name, tag, registry credentials |
OOMKilled |
Out of memory | Increase memory limits in Pod spec |
CreateContainerError |
Container config error | kubectl describe pod — check ConfigMap/Secret refs |
When you need to inspect the JSON output from kubectl get -o json or kubectl describe, the JSON Formatter makes complex Kubernetes resource definitions readable.
Helm: Package Management
Helm is the package manager for Kubernetes. It bundles related manifests into charts — reusable, version-controlled packages that can be configured with different values for each environment.
Installing Helm
# Install Helm (macOS)
brew install helm
# Add the official charts repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Search for a chart
helm search repo postgresql
Using Helm Charts
# Install PostgreSQL with custom values
helm install my-postgres bitnami/postgresql \
--set auth.postgresPassword=mysecretpass \
--set primary.persistence.size=20Gi \
--namespace databases --create-namespace
# Install with a values file
helm install my-postgres bitnami/postgresql \
-f values-production.yaml \
--namespace databases
# List installed releases
helm list --all-namespaces
# Check release status
helm status my-postgres -n databases
# Upgrade a release (new values or chart version)
helm upgrade my-postgres bitnami/postgresql \
--set primary.persistence.size=50Gi \
-n databases
# Rollback to previous revision
helm rollback my-postgres 1 -n databases
# View release history
helm history my-postgres -n databases
# Uninstall a release
helm uninstall my-postgres -n databases
Creating Your Own Chart
# Create chart structure
helm create my-web-app
# Chart directory structure:
# my-web-app/
# Chart.yaml # Chart metadata
# values.yaml # Default configuration values
# templates/
# deployment.yaml # Deployment template
# service.yaml # Service template
# ingress.yaml # Ingress template
# _helpers.tpl # Template helpers
# NOTES.txt # Post-install instructions
# Test rendering without installing
helm template my-web-app ./my-web-app -f values-staging.yaml
# Lint the chart for errors
helm lint ./my-web-app
# Package the chart
helm package ./my-web-app
# Install from local chart
helm install my-release ./my-web-app -f values.yaml
When writing YAML for Helm values files and Kubernetes manifests, the YAML Editor validates your syntax in real time and catches indentation issues that cause cryptic Kubernetes errors.
1) Set resource requests and limits on every container. 2) Use readiness and liveness probes. 3) Run at least 2 replicas. 4) Use PodDisruptionBudgets. 5) Store secrets in a secrets manager. 6) Set up HPA for autoscaling. 7) Use namespaces to isolate environments. 8) Enable RBAC.
Related Developer Tools
Frequently Asked Questions
Docker is a container runtime that builds and runs individual containers. Kubernetes is a container orchestration platform that manages many containers across multiple machines. Docker answers the question "how do I package and run my application in a container?" Kubernetes answers "how do I deploy, scale, and manage hundreds of containers across a cluster of servers?" You typically use Docker to build container images and Kubernetes to deploy and orchestrate them in production. They are complementary tools, not competitors.
A Pod is the smallest deployable unit in Kubernetes. It wraps one or more containers that share the same network namespace (they can communicate via localhost) and storage volumes. You do not deploy containers directly in Kubernetes because Pods provide a higher level of abstraction: they handle co-located containers that must run together, shared storage between containers, and initialization logic through init containers. In practice, most Pods run a single application container. Multi-container Pods are used for sidecar patterns like log collectors, service meshes (Istio/Envoy), or config reloaders.
Use a Service of type LoadBalancer or an Ingress resource. A LoadBalancer Service provisions a cloud load balancer (on AWS, GCP, or Azure) that routes external traffic to your Pods. An Ingress resource provides HTTP/HTTPS routing with path-based and host-based rules, SSL termination, and is more cost-effective because one Ingress controller (like Nginx or Traefik) can route traffic to many services. For development, use kubectl port-forward to access a Pod or Service from your local machine without any external exposure. For production, Ingress with a TLS certificate (often managed by cert-manager) is the standard approach.
Kubernetes supports both manual and automatic scaling. Manual scaling changes the replica count directly: kubectl scale deployment myapp --replicas=5. Horizontal Pod Autoscaler (HPA) automatically adjusts the number of Pod replicas based on CPU usage, memory usage, or custom metrics. For example, an HPA can scale from 2 to 10 replicas when average CPU usage exceeds 70%. Vertical Pod Autoscaler (VPA) adjusts the CPU and memory requests of individual Pods. Cluster Autoscaler adds or removes nodes (virtual machines) from the cluster when Pods cannot be scheduled due to insufficient resources. These three autoscalers can work together for fully automated scaling from application level to infrastructure level.
Helm is a package manager for Kubernetes. It bundles related Kubernetes manifests (Deployments, Services, ConfigMaps, etc.) into reusable packages called charts. Instead of managing dozens of YAML files manually, you install a chart with one command: helm install myrelease mychart. Helm handles templating (different values for staging vs production), versioning (rollback to a previous release), and dependency management (a chart can depend on other charts). You need Helm when you deploy complex applications with many Kubernetes resources, want to share standardized deployments across teams, or want to install third-party software (databases, monitoring tools, ingress controllers) without writing all the YAML yourself.
CrashLoopBackOff means the container keeps starting and crashing. First, check the logs: kubectl logs <pod-name> --previous (the --previous flag shows logs from the last crashed instance). Check the Pod events: kubectl describe pod <pod-name> and look at the Events section at the bottom for error messages like image pull failures, OOMKilled (out of memory), or failed health checks. Common causes include: missing environment variables or ConfigMaps, incorrect command or entrypoint in the container image, the application crashing due to a bug or unhandled exception, liveness probes failing too aggressively, and insufficient memory limits causing OOMKill. If the container exits too quickly to read logs, add a sleep command to the container spec temporarily: command: ['sh', '-c', 'sleep 3600'] to keep it running while you exec into it and investigate.