You've containerized your application with Docker. It runs perfectly on your laptop. But when you try to deploy it to production, you realize: running containers at scale is an entirely different challenge.
This guide walks you through Kubernetes—the industry-standard container orchestration platform—from core concepts to production deployment. Whether you're migrating from Docker Compose or building cloud-native applications from scratch, you'll learn what Kubernetes actually does, when you need it, and how to avoid the mistakes that lead to downtime and cost overruns.
1. The Container Orchestration Problem
Why docker run Doesn't Scale
Running a single container locally is straightforward:
docker run -p 3000:3000 my-appBut production introduces complexity that Docker alone can't solve:
- What happens when your container crashes? Who restarts it?
- How do you run 50 copies across multiple servers for high availability?
- How do you route traffic to healthy containers during deployments?
- How do you handle server failures without downtime?
- How do you manage configuration across environments (dev, staging, prod)?
- How do you scale automatically when traffic spikes?
What this means for your infrastructure: You need orchestration—a system that manages container lifecycle, networking, scaling, and self-healing automatically.
The Scaling Challenge: From 1 Container to 100+
Let's trace what happens as your application grows:
Stage 1: Single Container
- One server, one container
- Manual restarts when it crashes
- No redundancy
Stage 2: Docker Compose (5-10 containers)
- Multi-container applications
- Basic networking between services
- Still single-host (can't span multiple servers)
- Manual scaling and updates
Stage 3: Kubernetes (10-1000+ containers)
- Multi-host orchestration
- Automatic scaling and self-healing
- Zero-downtime deployments
- Service discovery and load balancing
- Declarative configuration
Real mistake we've seen—and how to avoid it: Developers jump straight to Kubernetes for simple applications. If you're running fewer than 5 services with predictable traffic, Docker Compose or managed platforms (Render, Railway, Cloud Run) are often simpler and cheaper.
Docker Compose vs Kubernetes vs Managed Solutions
| Solution | Best For | Complexity | Scaling Limit |
|---|---|---|---|
| Docker Compose | Local dev, simple apps | Low | Single host |
| Kubernetes | Production at scale | High | Multi-host, unlimited |
| Managed K8s (EKS, GKE, AKS) | Production without ops overhead | Medium | Multi-host, unlimited |
| Serverless (Cloud Run, Fargate) | Stateless apps, variable traffic | Low | Auto-scales infinitely |
What this means for your infrastructure: Start with the simplest solution that meets your requirements. Kubernetes adds operational complexity—make sure you need it.
2. Kubernetes Core Concepts
Cluster Architecture: Control Plane vs Worker Nodes
A Kubernetes cluster consists of two types of machines:
Control Plane (Master Nodes):
- API Server: Entry point for all cluster operations
- Scheduler: Decides which worker node runs each pod
- Controller Manager: Maintains desired state (restarts crashed pods, scales deployments)
- etcd: Distributed database storing cluster configuration
Worker Nodes:
- Kubelet: Agent that runs containers on the node
- Container Runtime: Docker, containerd, or CRI-O
- Kube-proxy: Handles networking rules
What happens behind the scenes: When you deploy an application, the API server stores the desired state in etcd. The scheduler assigns pods to nodes. The kubelet on each node pulls container images and starts them. Controllers continuously monitor the cluster and take corrective action (e.g., restarting failed pods).
Pods: The Smallest Deployable Unit (Not Containers)
Critical concept: In Kubernetes, you don't deploy containers directly. You deploy Pods—which contain one or more containers that share:
- Network namespace (same IP address)
- Storage volumes
- Lifecycle (start/stop together)
Most common pattern: One container per pod.
When to use multi-container pods:
- Sidecar pattern: Logging agent alongside your app
- Ambassador pattern: Proxy for external services
- Adapter pattern: Standardizing output format
apiVersion: v1
kind: Pod
metadata:
name: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 3000
- name: log-shipper # Sidecar container
image: fluent-bit:latestReal mistake we've seen—and how to avoid it: Developers create pods directly. Don't. Use Deployments instead (explained below)—they provide self-healing, scaling, and updates.
Services: How Containers Communicate
Pods are ephemeral—they get new IP addresses when restarted. Services provide stable networking:
ClusterIP (default):
- Internal load balancer
- Accessible only within the cluster
- Use for backend services, databases
NodePort:
- Exposes service on each node's IP at a static port
- Use for development or when you control the infrastructure
LoadBalancer:
- Creates an external load balancer (AWS ELB, GCP Load Balancer)
- Use for production external services
- Warning: Can be expensive—each LoadBalancer costs ~$20/month
ExternalName:
- Maps service to external DNS name
- Use for accessing external databases
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
type: ClusterIP
selector:
app: my-app
ports:
- port: 80
targetPort: 3000What happens behind the scenes: Kubernetes creates a virtual IP for the service. Kube-proxy configures iptables rules on every node to route traffic to healthy pods matching the selector.
Deployments: Declarative Application Management
Deployments manage ReplicaSets, which manage Pods. This three-layer abstraction enables:
- Self-healing: Restarts crashed pods
- Scaling: Runs multiple replicas
- Rolling updates: Zero-downtime deployments
- Rollbacks: Revert to previous versions
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:1.0
ports:
- containerPort: 3000
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"What this means for your infrastructure: You declare the desired state (3 replicas). Kubernetes continuously works to maintain it—if a pod crashes, a new one starts automatically.
ConfigMaps and Secrets: Configuration Management
ConfigMaps: Non-sensitive configuration (feature flags, API endpoints)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
API_URL: "https://api.example.com"
LOG_LEVEL: "info"Secrets: Sensitive data (passwords, API keys, certificates)
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=secret123Using them in pods:
containers:
- name: app
image: my-app:1.0
env:
- name: API_URL
valueFrom:
configMapKeyRef:
name: app-config
key: API_URL
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: passwordSecurity warning: Secrets are base64-encoded, not encrypted by default. For production:
- Enable encryption at rest in etcd (AWS KMS integration)
- Use external secret managers (AWS Secrets Manager, HashiCorp Vault)
- Implement RBAC to restrict secret access
Namespaces: Logical Resource Isolation
Namespaces divide a cluster into virtual sub-clusters:
kubectl create namespace production
kubectl create namespace stagingUse cases:
- Environment separation: dev, staging, prod
- Team isolation: frontend-team, backend-team
- Resource quotas: Limit CPU/memory per namespace
Real mistake we've seen—and how to avoid it: Not using namespaces in shared clusters. Without them, name collisions occur and there's no resource isolation. Production workloads can starve staging resources.
The Declarative Model: Desired State vs Imperative Commands
Imperative (avoid in production):
kubectl run my-app --image=my-app:1.0
kubectl scale deployment my-app --replicas=5Declarative (recommended):
kubectl apply -f deployment.yamlWhy declarative wins:
- Configuration is version-controlled
- Changes are auditable
- Easy to replicate environments
- GitOps workflows possible
3. Your First Kubernetes Deployment
Local Development Setup
Option 1: Minikube (Most popular)
# Install Minikube
brew install minikube # macOS
# or: https://minikube.sigs.k8s.io/docs/start/
# Start cluster
minikube start --cpus=4 --memory=8192
# Verify
kubectl get nodesOption 2: Kind (Kubernetes in Docker)
# Install Kind
brew install kind
# Create cluster
kind create cluster --name devOption 3: Docker Desktop
- Enable Kubernetes in settings
- Simplest for Docker users
If you're using Windows: Use WSL2 for better performance. Avoid running Kubernetes in VirtualBox.
Writing Your First Deployment YAML
Create deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-app
labels:
app: hello
spec:
replicas: 2
selector:
matchLabels:
app: hello
template:
metadata:
labels:
app: hello
spec:
containers:
- name: hello
image: gcr.io/google-samples/hello-app:1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: hello-service
spec:
type: LoadBalancer
selector:
app: hello
ports:
- port: 80
targetPort: 8080Understanding kubectl Commands
# Apply configuration
kubectl apply -f deployment.yaml
# Check deployment status
kubectl get deployments
kubectl get pods
kubectl get services
# Detailed pod information
kubectl describe pod <pod-name>
# Watch real-time updates
kubectl get pods --watch
# Delete resources
kubectl delete -f deployment.yamlEssential commands reference: Official kubectl Cheat Sheet
Exposing Your Application
ClusterIP (internal only):
spec:
type: ClusterIPNodePort (development):
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
nodePort: 30080 # Accessible at <node-ip>:30080LoadBalancer (production):
spec:
type: LoadBalancerFor Minikube:
# LoadBalancer won't get external IP in Minikube
# Use tunnel instead:
minikube service hello-serviceViewing Logs and Debugging Pods
# View logs
kubectl logs <pod-name>
kubectl logs -f <pod-name> # Follow mode
# Logs from specific container in multi-container pod
kubectl logs <pod-name> -c <container-name>
# Execute commands in running pod
kubectl exec -it <pod-name> -- /bin/sh
# Copy files from pod
kubectl cp <pod-name>:/path/to/file ./local-file
# Port forward for local testing
kubectl port-forward <pod-name> 8080:8080Real mistake we've seen—and how to avoid it: Forgetting to check logs when pods fail. Always start with kubectl describe pod and kubectl logs—they reveal 90% of issues.
4. Real-World Patterns
Rolling Updates and Rollbacks
Rolling update strategy:
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # Max extra pods during update
maxUnavailable: 0 # Ensure zero downtimeUpdate image:
kubectl set image deployment/hello-app hello=gcr.io/google-samples/hello-app:2.0
# Check rollout status
kubectl rollout status deployment/hello-app
# View rollout history
kubectl rollout history deployment/hello-appRollback to previous version:
kubectl rollout undo deployment/hello-app
# Rollback to specific revision
kubectl rollout undo deployment/hello-app --to-revision=2What happens behind the scenes: Kubernetes creates a new ReplicaSet with the new image version. It gradually scales up the new ReplicaSet while scaling down the old one, ensuring the total number of replicas remains constant.
Health Checks: Liveness vs Readiness Probes
Liveness Probe: "Is the container alive?"
- If it fails, Kubernetes restarts the container
- Use for detecting deadlocks or hung processes
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3Readiness Probe: "Is the container ready to receive traffic?"
- If it fails, Kubernetes removes the pod from service endpoints
- Use during startup, database migrations, or temporary failures
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5Real mistake we've seen—and how to avoid it: No health checks, or using the same endpoint for both probes. During deployment, new pods receive traffic before they're ready, causing errors. Use different endpoints: liveness checks core functionality, readiness checks dependencies (database, cache).
Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Implement /healthz and /ready endpoints in your application. Return 200 for healthy, 503 for unhealthy. Include dependency checks in readiness (database connection, Redis availability).
Resource Limits and Requests (Preventing Node Crashes)
Requests: Guaranteed resources (used for scheduling) Limits: Maximum resources (enforced by kernel)
resources:
requests:
cpu: "250m" # 0.25 CPU cores
memory: "512Mi" # 512 megabytes
limits:
cpu: "1000m" # 1 CPU core
memory: "1Gi" # 1 gigabyteWhat happens behind the scenes:
- Kubernetes schedules pods only on nodes with available requested resources
- If a pod exceeds memory limit, it's killed (OOMKilled)
- If a pod exceeds CPU limit, it's throttled (slower, not killed)
Real mistake we've seen—and how to avoid it: Not setting resource limits. A single pod with a memory leak can crash an entire node, taking down all pods on it. Always set limits.
If you're using AWS EKS or GKE, here's what to watch for: Use Vertical Pod Autoscaler to analyze actual resource usage and recommend right-sized requests/limits.
Horizontal Pod Autoscaling
Automatically scale based on CPU, memory, or custom metrics:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: hello-app-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: hello-app
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70Apply metrics server (required for HPA):
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yamlAdvanced scaling: Use KEDA for event-driven autoscaling (queue length, database connections, custom metrics).
StatefulSets for Databases (When You Need Persistent Storage)
Use StatefulSets for applications requiring:
- Stable network identities (predictable pod names)
- Persistent storage (data survives pod restarts)
- Ordered deployment and scaling
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
spec:
serviceName: postgres
replicas: 3
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15
ports:
- containerPort: 5432
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiWhat this means for your infrastructure: Pods are named predictably (postgres-0, postgres-1, postgres-2). Each pod gets its own persistent volume that survives restarts.
Real mistake we've seen—and how to avoid it: Running production databases in Kubernetes without understanding storage classes and backup strategies. For small teams, managed databases (RDS, Cloud SQL) are often simpler and more reliable.
Jobs and CronJobs for Batch Processing
Job (one-time task):
apiVersion: batch/v1
kind: Job
metadata:
name: data-migration
spec:
template:
spec:
containers:
- name: migrate
image: my-app:1.0
command: ["node", "migrate.js"]
restartPolicy: OnFailureCronJob (scheduled task):
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: my-backup:1.0
command: ["./backup.sh"]
restartPolicy: OnFailure
```
---
## 5. Kubernetes Networking Demystified
### How Pod-to-Pod Communication Works
**Every pod gets its own IP address.** Pods can communicate directly without NAT.
**Network model:**
1. Pods on the same node communicate via virtual Ethernet bridge
2. Pods on different nodes communicate via overlay network (Calico, Flannel, Weave)
**What happens behind the scenes:** The Container Network Interface (CNI) plugin configures network routes. Traffic between pods flows through the CNI without leaving the cluster network.
### Service Discovery and DNS
Kubernetes runs an internal DNS server (CoreDNS). Services are automatically registered.
**DNS format:**
```
<service-name>.<namespace>.svc.cluster.localExamples:
# Within the same namespace
curl http://backend-service
# Across namespaces
curl http://database.production.svc.cluster.local
```
**Environment variables (alternative to DNS):**
Kubernetes injects service endpoints as environment variables:
```
BACKEND_SERVICE_HOST=10.96.0.5
BACKEND_SERVICE_PORT=8080Ingress Controllers for External Traffic
Ingress routes external HTTP/HTTPS traffic to services:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: app.example.com
http:
paths:
- path: /api
pathType: Prefix
backend:
service:
name: backend-service
port:
number: 8080
- path: /
pathType: Prefix
backend:
service:
name: frontend-service
port:
number: 80
tls:
- hosts:
- app.example.com
secretName: app-tls-certPopular ingress controllers:
- NGINX Ingress: Most widely used
- Traefik: Built-in Let's Encrypt support
- AWS ALB Ingress: Native AWS integration
- Istio Gateway: Service mesh features
Install NGINX Ingress:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yamlIf you're using AWS, here's what to watch for: Use the AWS Load Balancer Controller for native ALB/NLB integration. It's more cost-effective than creating a LoadBalancer service per application.
Network Policies for Security
By default, all pods can communicate. NetworkPolicies implement zero-trust networking:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- protocol: TCP
port: 5432What this means for your infrastructure: Backend pods can only receive traffic from frontend pods and can only send traffic to database pods.
Real mistake we've seen—and how to avoid it: Enabling network policies without testing. If misconfigured, you can lock yourself out or break internal communication. Test in staging first.
6. Storage and Persistence
Volumes vs Persistent Volumes vs Persistent Volume Claims
Volume: Temporary storage tied to pod lifecycle
volumes:
- name: cache
emptyDir: {}Persistent Volume (PV): Cluster-level storage resource Persistent Volume Claim (PVC): Request for storage by a user
Workflow:
- Admin provisions PV (or uses dynamic provisioning)
- User creates PVC requesting storage
- Kubernetes binds PVC to matching PV
- Pod mounts PVC as volume
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-storage
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
storageClassName: standard
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: app-storageWhen to Use StatefulSets vs Deployments
Use Deployments when:
- Application is stateless
- Any pod can handle any request
- Pods are interchangeable
Use StatefulSets when:
- Application requires stable network identity
- Pods need persistent storage
- Pods must start/stop in order (e.g., database replicas)
Storage Classes and Dynamic Provisioning
Storage Classes define types of storage:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
type: gp3
iopsPerGB: "10"
allowVolumeExpansion: trueCloud provider defaults:
- AWS:
gp2(EBS volumes) - GCP:
pd-standard(Persistent Disks) - Azure:
default(Azure Disk)
Real mistake we've seen—and how to avoid it: Using default storage classes for production databases. They're often slow. Use SSD-backed classes (gp3 on AWS, pd-ssd on GCP).
7. Development Workflow
Local Development with Kubernetes
Option 1: Skaffold (auto-rebuild on code changes)
# skaffold.yaml
apiVersion: skaffold/v2beta29
kind: Config
build:
artifacts:
- image: my-app
docker:
dockerfile: Dockerfile
deploy:
kubectl:
manifests:
- k8s/*.yamlskaffold dev # Watch mode: rebuilds on file changesOption 2: Tilt (visual dashboard + hot reload)
# Tiltfile
docker_build('my-app', '.')
k8s_yaml('k8s/deployment.yaml')
k8s_resource('my-app', port_forwards=8080)tilt upOption 3: DevSpace (development containers in cluster)
devspace devIf you're using VS Code: Install the Kubernetes extension for YAML validation and IntelliSense.
CI/CD Integration Patterns
GitHub Actions example:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build image
run: docker build -t my-app:${{ github.sha }} .
- name: Push to registry
run: |
docker tag my-app:${{ github.sha }} gcr.io/my-project/my-app:${{ github.sha }}
docker push gcr.io/my-project/my-app:${{ github.sha }}
- name: Deploy to Kubernetes
run: |
kubectl set image deployment/my-app my-app=gcr.io/my-project/my-app:${{ github.sha }}Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use image digests instead of tags for deployments. Tags are mutable; digests are immutable and guarantee reproducible deployments.
GitOps Approach (ArgoCD, FluxCD)
GitOps principle: Git repository is the single source of truth for cluster state.
ArgoCD workflow:
- Push Kubernetes manifests to Git
- ArgoCD monitors repository
- ArgoCD automatically syncs changes to cluster
- Drift detection: alerts if cluster state diverges from Git
Install ArgoCD:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yamlCreate application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/my-app
targetRevision: HEAD
path: k8s
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: trueWhat this means for your infrastructure: Changes to production happen through pull requests. No manual kubectl commands. Full audit trail in Git history.
Helm Charts: Packaging Kubernetes Applications
Helm is a package manager for Kubernetes. Charts are reusable application templates.
Install Helm:
brew install helmCreate chart:
helm create my-app
```
**Chart structure:**
```
my-app/
├── Chart.yaml # Chart metadata
├── values.yaml # Default configuration
└── templates/
├── deployment.yaml
├── service.yaml
└── ingress.yamlvalues.yaml:
image:
repository: my-app
tag: "1.0"
replicaCount: 2
service:
type: ClusterIP
port: 80Install chart:
helm install my-app ./my-app --set replicaCount=3Use Helm for:
- Deploying complex applications (PostgreSQL, Redis, Kafka)
- Managing multiple environments (values-dev.yaml, values-prod.yaml)
- Versioning application releases
Find charts: Artifact Hub
8. Common Mistakes and Gotchas
Not Setting Resource Limits (Crashing Entire Nodes)
Symptom: Node runs out of memory, kubelet starts evicting pods randomly.
Root cause: Pods without memory limits can consume all node resources.
Fix: Always set requests and limits:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"If you're using managed Kubernetes: Enable Limit Ranges to enforce default limits on all pods in a namespace.
Missing Health Checks (Traffic to Unhealthy Pods)
Symptom: Intermittent 502 errors during deployments.
Root cause: New pods receive traffic before they're ready.
Fix: Implement readiness probes:
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 10
periodSeconds: 5Using latest Tag in Production
Symptom: Deployments are not reproducible. Rollbacks don't work as expected.
Root cause: latest tag is mutable. Different nodes might pull different versions.
Fix: Use immutable tags (commit SHAs or semantic versions):
image: my-app:v1.2.3
# or
image: my-app:abc123def # Git commit SHANot Understanding Pod Restart Policies
Policies:
Always(default for Deployments): Restart on any exitOnFailure(Jobs): Restart only on non-zero exitNever: Never restart
Real mistake we've seen—and how to avoid it: Using Always for Jobs or init containers. This causes infinite restart loops if the task fails.
Misunderstanding Service Types
Common confusion: "I set type: LoadBalancer but can't access the service."
Troubleshooting:
- Check external IP:
kubectl get svc - If
<pending>, your cluster doesn't support LoadBalancer (use NodePort or Ingress) - Verify security groups / firewall rules
Security: Running as Root, Exposing Secrets
Symptom: Container compromise leads to node compromise.
Root cause: Containers running as root have elevated privileges.
Fix: Run as non-root user:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: app
image: my-app:1.0
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALLDockerfile best practice:
FROM node:18-alpine
# Create non-root user
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# Install dependencies as root
COPY package*.json ./
RUN npm ci --only=production
# Switch to non-root user
USER appuser
COPY . .
CMD ["node", "server.js"]Real mistake we've seen—and how to avoid it: Hardcoding secrets in environment variables or ConfigMaps. Use Kubernetes Secrets, and for production, integrate with external secret managers:
- AWS: External Secrets Operator + AWS Secrets Manager
- GCP: Workload Identity + Secret Manager
- HashiCorp Vault: Vault Agent Injector
Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Scan container images for vulnerabilities using Trivy or Grype. Integrate into CI/CD to block vulnerable images from deployment.
9. When Kubernetes Is Overkill
Do You Actually Need Kubernetes?
Kubernetes adds complexity. Before adopting, ask:
- Are you running multiple services? (Single apps rarely need orchestration)
- Do you need to scale dynamically? (Predictable traffic doesn't require autoscaling)
- Do you have operational expertise? (Kubernetes requires monitoring, security, upgrades)
- Is your team large enough? (Small teams benefit more from managed platforms)
What this means for your infrastructure: If you're a startup with 2-3 developers running 5 microservices, Kubernetes is probably overkill.
Simpler Alternatives
For small applications (1-3 services):
- Render, Railway, Fly.io: Deploy from Git, auto-scaling, zero config
- AWS App Runner, Google Cloud Run: Serverless containers
- Heroku, DigitalOcean App Platform: Managed PaaS
For Docker Compose users:
- AWS ECS Fargate: Run containers without managing servers
- Docker Swarm: Simpler orchestration (though less popular)
For predictable workloads:
- Traditional VPS (DigitalOcean, Linode): Simple, cost-effective
- Managed Kubernetes (EKS Autopilot, GKE Autopilot): Kubernetes without infrastructure management
When Kubernetes Makes Sense
You should use Kubernetes if:
- Running 10+ microservices
- Need multi-cloud or hybrid deployments
- Require complex networking (service mesh, advanced routing)
- Traffic patterns require rapid scaling (10x spikes)
- Team has dedicated DevOps/platform engineers
- Building internal developer platform
Real-world examples:
- Spotify: Runs 1,000+ services on Kubernetes across multiple clouds
- Airbnb: Uses Kubernetes for dynamic scaling during travel surges
- Reddit: Migrated from EC2 to Kubernetes for better resource utilization
What this means for your infrastructure: Kubernetes is an infrastructure investment. Start with managed services to reduce operational burden.
10. Production-Ready Deployment Checklist
Architecture Decisions
Managed vs Self-Hosted:
- EKS (AWS): Deep AWS integration, ALB/EBS support, IAM for pods (setup guide)
- GKE (GCP): Best Kubernetes experience, Autopilot mode, built-in monitoring (setup guide)
- AKS (Azure): Azure native, Active Directory integration (setup guide)
- Self-hosted: Full control, but requires expertise in etcd, networking, certificate management
If you're using AWS, here's what to watch for: Use EKS Fargate for serverless pods (no node management). Use Spot Instances for dev/staging to save 70% on costs.
Essential Add-ons for Production
Monitoring & Logging:
# Prometheus + Grafana
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack
# ELK Stack for logs
helm repo add elastic https://helm.elastic.co
helm install elasticsearch elastic/elasticsearch
helm install kibana elastic/kibanaOr use managed services:
- Datadog: Unified monitoring, APM, logs
- New Relic: Full observability platform
- AWS CloudWatch Container Insights: Native AWS integration
Ingress & TLS:
# NGINX Ingress + cert-manager (Let's Encrypt)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager --set installCRDs=trueBackup & Disaster Recovery:
# Velero for cluster backups
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm install velero vmware-tanzu/velero \
--set configuration.provider=aws \
--set configuration.backupStorageLocation.bucket=my-backup-bucketSecurity Scanning:
- Falco: Runtime security monitoring
- OPA Gatekeeper: Policy enforcement
- Kyverno: Kubernetes-native policy engine
Cost Optimization Strategies
Node Autoscaling:
# Cluster Autoscaler
apiVersion: autoscaling/v1
kind: ClusterAutoscaler
spec:
minNodes: 2
maxNodes: 10Spot Instances (AWS):
- Use for stateless workloads, dev environments
- Save 70-90% vs on-demand pricing
- Configure multiple instance types for availability
Right-Sizing:
# Install Goldilocks for recommendations
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install goldilocks fairwinds-stable/goldilocksNamespace Resource Quotas:
apiVersion: v1
kind: ResourceQuota
metadata:
name: compute-quota
namespace: development
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
persistentvolumeclaims: "10"Real mistake we've seen—and how to avoid it: Running production workloads on oversized nodes. Use multiple smaller nodes for better bin-packing and fault tolerance. T3.medium (2 vCPU, 4GB) is often better than T3.xlarge (4 vCPU, 16GB) for most workloads.
11. Real-World Deployment Scenarios
Scenario 1: Deploying a Node.js API
Architecture:
- Node.js Express API
- PostgreSQL database (managed RDS)
- Redis cache (ElastiCache)
- Environment-specific configuration
Step 1: Create Dockerfile
FROM node:18-alpine
WORKDIR /app
# Install dependencies
COPY package*.json ./
RUN npm ci --only=production
# Add non-root user
RUN addgroup -g 1000 node && adduser -D -u 1000 -G node node
USER node
# Copy application
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Step 2: Build and push image
docker build -t my-api:v1.0.0 .
docker tag my-api:v1.0.0 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0Step 3: Create ConfigMap for non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
namespace: production
data:
NODE_ENV: "production"
LOG_LEVEL: "info"
API_RATE_LIMIT: "100"Step 4: Create Secret for database credentials
kubectl create secret generic db-credentials \
--from-literal=DB_HOST=mydb.123456.us-east-1.rds.amazonaws.com \
--from-literal=DB_USER=api_user \
--from-literal=DB_PASSWORD='SecureP@ssw0rd!' \
--from-literal=REDIS_URL=redis://mycache.abc123.0001.use1.cache.amazonaws.com:6379 \
--namespace productionStep 5: Create Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: production
labels:
app: api
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
version: v1.0.0
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: api
image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0
imagePullPolicy: Always
ports:
- containerPort: 3000
name: http
env:
- name: PORT
value: "3000"
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: db-credentials
resources:
requests:
cpu: "250m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 10
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 2
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: api-service
namespace: production
spec:
type: ClusterIP
selector:
app: api
ports:
- port: 80
targetPort: 3000
protocol: TCP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80Step 6: Create Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-ingress
namespace: production
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/rate-limit: "100"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service
port:
number: 80Step 7: Deploy
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/ingress.yaml
# Verify deployment
kubectl rollout status deployment/api -n production
kubectl get pods -n production -l app=apiWhat this means for your infrastructure: Your API runs with 3 replicas for high availability, automatically scales to 20 replicas under load, has health checks to prevent traffic to unhealthy pods, and runs with security best practices (non-root user, read-only filesystem).
Scenario 2: Multi-Service Application (Frontend + Backend + Database)
Architecture:
- React frontend (static files served by NGINX)
- Node.js backend API
- PostgreSQL database (StatefulSet with persistent storage)
- All services communicate internally
Frontend Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
namespace: production
spec:
replicas: 2
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: nginx
image: my-frontend:v1.0.0
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "200m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: frontend
namespace: production
spec:
selector:
app: frontend
ports:
- port: 80
targetPort: 80Backend Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: api
image: my-backend:v1.0.0
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
value: "postgresql://postgres.production.svc.cluster.local:5432/mydb"
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: production
spec:
selector:
app: backend
ports:
- port: 8080
targetPort: 8080PostgreSQL StatefulSet:
apiVersion: v1
kind: Service
metadata:
name: postgres
namespace: production
spec:
clusterIP: None # Headless service
selector:
app: postgres
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: production
spec:
serviceName: postgres
replicas: 1
selector:
matchLabels:
app: postgres
template:
metadata:
labels:
app: postgres
spec:
containers:
- name: postgres
image: postgres:15-alpine
ports:
- containerPort: 5432
env:
- name: POSTGRES_DB
value: mydb
- name: POSTGRES_USER
value: postgres
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-storage
mountPath: /var/lib/postgresql/data
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "1000m"
memory: "2Gi"
volumeClaimTemplates:
- metadata:
name: postgres-storage
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3 # AWS EBS gp3
resources:
requests:
storage: 20GiIngress routing:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
namespace: production
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- host: myapp.example.com
http:
paths:
- path: /api(/|$)(.*)
pathType: Prefix
backend:
service:
name: backend
port:
number: 8080
- path: /()(.*)
pathType: Prefix
backend:
service:
name: frontend
port:
number: 80What happens behind the scenes: Frontend makes API calls to /api/*, which Ingress routes to the backend service. Backend connects to PostgreSQL using internal DNS (postgres.production.svc.cluster.local). No external exposure of the database.
Real mistake we've seen—and how to avoid it: Running production databases in Kubernetes without backup strategies. Implement automated backups:
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-backup
namespace: production
spec:
schedule: "0 2 * * *" # Daily at 2 AM
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: postgres:15-alpine
command:
- /bin/sh
- -c
- |
pg_dump -h postgres.production.svc.cluster.local -U postgres mydb | \
gzip > /backup/backup-$(date +%Y%m%d-%H%M%S).sql.gz
aws s3 cp /backup/*.sql.gz s3://my-backups/postgres/
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
volumeMounts:
- name: backup
mountPath: /backup
volumes:
- name: backup
emptyDir: {}
restartPolicy: OnFailureScenario 3: Background Job Processing (Queue-Based Workers)
Architecture:
- RabbitMQ message queue
- Multiple worker pods processing jobs
- CronJob for scheduled tasks
RabbitMQ Deployment:
# Install using Helm
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq \
--set auth.username=admin \
--set auth.password=SecurePassword123 \
--namespace productionWorker Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
namespace: production
spec:
replicas: 5
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
spec:
containers:
- name: worker
image: my-worker:v1.0.0
env:
- name: RABBITMQ_URL
value: "amqp://admin:SecurePassword123@rabbitmq.production.svc.cluster.local:5672"
- name: QUEUE_NAME
value: "jobs"
- name: CONCURRENCY
value: "10"
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: worker-hpa
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: worker
minReplicas: 5
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80CronJob for scheduled tasks:
apiVersion: batch/v1
kind: CronJob
metadata:
name: daily-report
namespace: production
spec:
schedule: "0 8 * * *" # Daily at 8 AM
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
containers:
- name: report-generator
image: my-worker:v1.0.0
command: ["node", "generate-report.js"]
env:
- name: REPORT_TYPE
value: "daily"
- name: S3_BUCKET
value: "my-reports"
restartPolicy: OnFailureWhat this means for your infrastructure: Workers automatically scale based on CPU usage (queue depth can also be used with custom metrics). CronJobs run scheduled tasks reliably without manual intervention.
12. Debugging Strategies When Things Go Wrong
Common Debugging Commands
# Get high-level cluster status
kubectl get nodes
kubectl get pods --all-namespaces
kubectl top nodes
kubectl top pods
# Describe resources for detailed info
kubectl describe pod <pod-name>
kubectl describe node <node-name>
kubectl describe service <service-name>
# View logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous # Logs from crashed container
kubectl logs <pod-name> -c <container-name> # Specific container
kubectl logs -f <pod-name> # Follow logs
# Execute commands in pod
kubectl exec -it <pod-name> -- /bin/sh
kubectl exec <pod-name> -- env # View environment variables
kubectl exec <pod-name> -- ps aux # View processes
# Port forwarding for local access
kubectl port-forward <pod-name> 8080:8080
kubectl port-forward service/<service-name> 8080:80
# Events (often reveals issues)
kubectl get events --sort-by='.lastTimestamp'
kubectl get events --field-selector involvedObject.name=<pod-name>Debugging "My Pod Keeps Crashing"
Step 1: Check pod status
kubectl get podsCommon statuses:
- CrashLoopBackOff: Container crashes repeatedly
- ImagePullBackOff: Can't pull container image
- Pending: Waiting to be scheduled
- OOMKilled: Out of memory
Step 2: Describe the pod
kubectl describe pod <pod-name>Look for:
- Events section: Error messages
- Last State: Reason for previous crash
- Conditions: Why pod isn't ready
Step 3: Check logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous # Critical for crash loops
```
**Common causes and fixes:**
**ImagePullBackOff:**
```
Error: Failed to pull image "my-app:1.0.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied
```
**Fix:** Check image name, registry authentication, or image exists
**OOMKilled:**
```
Last State: Terminated
Reason: OOMKilled
Exit Code: 137
```
**Fix:** Increase memory limits or optimize application memory usage
**CrashLoopBackOff with immediate exit:**
```
Error: failed to start container "app": Error response from daemon: OCI runtime create failedFix: Check Dockerfile CMD/ENTRYPOINT, ensure binary exists and has execute permissions
Debugging "I Can't Reach My Service"
Step 1: Verify service exists and has endpoints
kubectl get service <service-name>
kubectl get endpoints <service-name>If no endpoints: Service selector doesn't match any pods
# Check pod labels
kubectl get pods --show-labels
# Compare with service selector
kubectl get service <service-name> -o yaml | grep selector -A 2Step 2: Test internal connectivity
# Create debug pod
kubectl run debug --image=nicolaka/netshoot -it --rm
# Inside debug pod:
curl http://<service-name>.<namespace>.svc.cluster.local
nslookup <service-name>.<namespace>.svc.cluster.localStep 3: Check network policies
kubectl get networkpolicies
kubectl describe networkpolicy <policy-name>Step 4: Verify port configuration
# Check service ports
kubectl get service <service-name> -o yaml
# Check pod ports
kubectl get pod <pod-name> -o yaml | grep -A 5 portsReal mistake we've seen—and how to avoid it: Port mismatch between service and container. Service targets port 8080, but container listens on 3000. Always verify targetPort matches containerPort.
Debugging Tools
k9s: Terminal UI for Kubernetes
brew install k9s
k9sLens: Desktop IDE for Kubernetes
- Download: https://k8slens.dev/
- Visual pod management, log streaming, terminal access
Stern: Multi-pod log tailing
brew install stern
stern <pod-name-prefix> # Tails logs from all matching podskubectx/kubens: Context and namespace switching
brew install kubectx
kubectx # Switch clusters
kubens # Switch namespaces13. Security Best Practices
RBAC (Role-Based Access Control) Fundamentals
Principle: Grant minimum necessary permissions.
Create read-only user:
apiVersion: v1
kind: ServiceAccount
metadata:
name: readonly-user
namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: readonly-role
namespace: production
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: readonly-binding
namespace: production
subjects:
- kind: ServiceAccount
name: readonly-user
namespace: production
roleRef:
kind: Role
name: readonly-role
apiGroup: rbac.authorization.k8s.ioFor cluster-wide permissions, use ClusterRole and ClusterRoleBinding.
Real mistake we've seen—and how to avoid it: Giving developers cluster-admin access. Create namespace-specific roles instead.
Pod Security Standards
Kubernetes Pod Security Standards (replacement for Pod Security Policies):
Levels:
- Privileged: Unrestricted (default)
- Baseline: Prevents known privilege escalations
- Restricted: Heavily restricted (production recommendation)
Enable at namespace level:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restrictedWhat this enforces:
- No root containers
- No privilege escalation
- No host networking/ports/IPC
- Read-only root filesystem
- Restricted volumes
Official documentation: Pod Security Standards
Network Policies for Zero-Trust Networking
Default deny all traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- EgressAllow specific traffic:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: production
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- protocol: TCP
port: 8080If you're using AWS EKS: Ensure you're using a CNI that supports network policies (Calico, Cilium). The default AWS VPC CNI doesn't support them.
Scanning Images for Vulnerabilities
Integrate Trivy into CI/CD:
# GitHub Actions
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: 'my-app:${{ github.sha }}'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail build on vulnerabilitiesScan running images:
trivy image --severity HIGH,CRITICAL my-app:1.0.0Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use admission controllers (OPA Gatekeeper, Kyverno) to block deployment of vulnerable images:
# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-vulnerable-images
spec:
validationFailureAction: enforce
rules:
- name: check-vulnerabilities
match:
resources:
kinds:
- Pod
validate:
message: "Image contains HIGH or CRITICAL vulnerabilities"
deny:
conditions:
- key: "{{ images.*.vulnerabilities | length(@) }}"
operator: GreaterThan
value: 014. Migration Strategy
Moving from Docker Compose to Kubernetes
Docker Compose:
version: '3'
services:
web:
image: my-app:latest
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://db:5432/mydb
depends_on:
- db
db:
image: postgres:15
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=secret
volumes:
postgres_data:Equivalent Kubernetes manifests:
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 1
selector: matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: my-app:latest
ports:
- containerPort: 3000
env:
- name: DATABASE_URL
value: "postgresql://db:5432/mydb"
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: LoadBalancer
selector:
app: web
ports:
- port: 80
targetPort: 3000
---
# db-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:15
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None # Headless service
selector:
app: db
ports:
- port: 5432Automated conversion tool:
# Kompose converts Docker Compose to Kubernetes manifests
brew install kompose
kompose convert -f docker-compose.ymlWhat this means for your infrastructure: Kompose generates a starting point, but you'll need to add production concerns: resource limits, health checks, proper secrets management, and storage classes.
Lift-and-Shift vs Cloud-Native Refactoring
Lift-and-Shift Approach:
- Containerize existing application as-is
- Deploy to Kubernetes with minimal changes
- Use StatefulSets for databases
- Keep external dependencies (RDS, Redis)
Pros: Fast migration, minimal code changes Cons: Doesn't leverage Kubernetes benefits, harder to scale
Cloud-Native Refactoring:
- Design for statelessness
- Externalize configuration (ConfigMaps, Secrets)
- Implement health check endpoints
- Use horizontal scaling
- Leverage service mesh (Istio, Linkerd)
- Adopt 12-factor app principles
Pros: Better scalability, resilience, observability Cons: Requires application changes, longer timeline
SimplifiedTechhub recommendation: Start with lift-and-shift for quick wins, then incrementally refactor. Prioritize:
- Health checks (immediate reliability improvement)
- Configuration externalization (easier environment management)
- Stateless design (enables autoscaling)
- Observability (structured logging, metrics, tracing)
Phased Migration Strategy
Phase 1: Non-Critical Services (Week 1-2)
- Choose low-traffic internal service
- Deploy to staging Kubernetes cluster
- Test thoroughly
- Monitor for 1 week
- Document lessons learned
Phase 2: Canary Production Deployment (Week 3-4)
- Deploy to production alongside existing infrastructure
- Route 10% of traffic to Kubernetes (using feature flags or load balancer)
- Gradually increase to 100%
- Keep rollback plan ready
Phase 3: Critical Services (Week 5-8)
- Apply lessons from Phase 1-2
- Migrate with blue-green deployment strategy
- Full monitoring and alerting
Phase 4: Database Migration (Week 9-12)
- Set up replication between old and new
- Test failover procedures
- Perform migration during maintenance window
- Keep old database as backup for 30 days
Real mistake we've seen—and how to avoid it: Big-bang migrations without fallback plans. Always maintain the ability to route traffic back to the old infrastructure during migration.
15. When to Start with Managed Services
Managed Kubernetes Options
GKE Autopilot (Google Cloud):
- What it manages: Nodes, upgrades, security patches
- You manage: Just your applications
- Pricing: Pay per pod resource usage
- Best for: Teams wanting zero infrastructure management
# Create Autopilot cluster
gcloud container clusters create-auto my-cluster \
--region=us-central1EKS Fargate (AWS):
- What it manages: EC2 instances, node groups
- You manage: Pod specifications
- Pricing: Pay per pod (0.25 vCPU minimum)
- Best for: Variable workloads, serverless architecture
# Create Fargate profile
eksctl create fargateprofile \
--cluster my-cluster \
--name production \
--namespace productionAKS with Virtual Nodes (Azure):
- What it manages: Underlying VMs
- You manage: Kubernetes resources
- Pricing: Per second billing
- Best for: Burst workloads
What this means for your infrastructure: Managed services reduce operational burden but cost more than self-managed nodes. Calculate break-even point based on team size and operational complexity.
Cost Comparison Example
Self-Managed EKS (3-node cluster):
- EC2 instances: 3 × t3.large × $0.0832/hr = $179/month
- EKS control plane: $73/month
- Total: ~$252/month + operational overhead
EKS Fargate:
- Pay per pod: varies by workload
- Example: 10 pods × 0.5 vCPU × $0.04/hr = $144/month
- Total: ~$144/month for small workloads, no operational overhead
Real-world insight: For teams under 10 developers, managed services often cost less when you factor in engineer time spent on cluster maintenance, security patches, and troubleshooting.
Alternative: Start with Simpler Solutions
Before jumping to Kubernetes:
For simple apps (1-3 services):
- Render: Git-push deployments, auto-scaling, free tier
- Railway: Instant deployments, GitHub integration
- Fly.io: Global edge deployment, simple pricing
For containerized workloads:
- AWS ECS Fargate: Simpler than Kubernetes, still scalable
- Google Cloud Run: Fully managed, pay per request
- Azure Container Instances: Quick container deployment
For teams with Docker Compose:
- DigitalOcean App Platform: Docker Compose compatibility
- Render (Docker): Compose-to-production
- Railway (Docker): Multi-container support
When to graduate to Kubernetes:
- Running 10+ interconnected services
- Need complex networking (service mesh, advanced routing)
- Require multi-cloud or hybrid deployments
- Team has dedicated platform/DevOps engineers
- Custom scaling requirements beyond standard autoscaling
16. Developer Tools That Make Kubernetes Less Painful
k9s: Terminal UI for Kubernetes
Install:
brew install k9sKey features:
- Live view of all resources
- Log streaming
- Shell into pods
- Resource editing
- Port forwarding
- Context switching
Keyboard shortcuts:
:pod- View pods:deploy- View deployments:svc- View servicesl- View logsd- Describe resourcee- Edit resources- Shell into pod
What this means for your infrastructure: Significantly faster than typing kubectl commands. Essential for daily Kubernetes work.
Lens: Kubernetes IDE
Download: https://k8slens.dev/
Key features:
- Visual cluster management
- Built-in terminal
- Metrics dashboard
- Helm chart deployment
- Resource templates
- Multi-cluster management
Best for: Developers who prefer GUI over CLI.
Skaffold: Continuous Development
Install:
brew install skaffoldskaffold.yaml:
apiVersion: skaffold/v4beta6
kind: Config
metadata:
name: my-app
build:
artifacts:
- image: my-app
docker:
dockerfile: Dockerfile
sync:
manual:
- src: 'src/**/*.js'
dest: /app/src
deploy:
kubectl:
manifests:
- k8s/*.yaml
portForward:
- resourceType: service
resourceName: my-app
port: 8080
localPort: 3000Development workflow:
# Auto-rebuild and redeploy on file changes
skaffold dev
# Build and deploy once
skaffold run
# Delete all resources
skaffold deleteWhat happens behind the scenes: Skaffold watches your source files, rebuilds the container image when they change, pushes to registry, and updates the deployment. File sync mode (for interpreted languages) copies changed files directly into running pods without rebuilding.
Tilt: Visual Development Environment
Install:
brew install tiltTiltfile:
# Build Docker image
docker_build('my-app', '.')
# Deploy Kubernetes manifests
k8s_yaml('k8s/deployment.yaml')
# Port forwarding
k8s_resource('my-app', port_forwards=3000)
# Live update (no rebuild needed)
docker_build('my-app', '.',
live_update=[
sync('./src', '/app/src'),
run('npm install', trigger='package.json')
]
)Start development:
tilt upAccess web UI: http://localhost:10350
Key features:
- Visual dashboard of all services
- Live logs from all pods
- Build status and timing
- Resource health indicators
- One-click restart
Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use Tilt for local development. It dramatically improves the feedback loop compared to manual docker build and kubectl apply cycles.
Telepresence: Local Development with Remote Services
Problem: Running entire microservices stack locally is resource-intensive.
Solution: Run one service locally, connect to remote cluster for dependencies.
Install:
brew install telepresenceConnect to cluster:
# Connect local machine to Kubernetes network
telepresence connect
# Run local service as if it's in the cluster
telepresence intercept my-app --port 3000What happens behind the scenes: Telepresence creates a VPN connection to your cluster. Traffic to the intercepted service is routed to your local machine. You can access remote services (databases, APIs) using cluster DNS names.
Use case: Debugging production issues locally with access to production dependencies (with proper safeguards).
17. Cost Optimization in Production
Right-Sizing Workloads
Problem: Developers often over-provision resources "to be safe."
Solution: Use Vertical Pod Autoscaler (VPA) for recommendations:
# Install VPA
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.shCreate VPA recommendation:
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: my-app-vpa
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
updateMode: "Off" # Recommendation only, doesn't auto-applyView recommendations:
kubectl describe vpa my-app-vpaWhat this means for your infrastructure: VPA analyzes actual resource usage and recommends optimal requests/limits. Can reduce costs by 30-50% by eliminating over-provisioning.
Cluster Autoscaling
AWS EKS:
# Install Cluster Autoscaler
kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml
# Annotate deployment with cluster name
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler \
cluster-autoscaler.kubernetes.io/safe-to-evict="false"GKE (built-in):
gcloud container clusters update my-cluster \
--enable-autoscaling \
--min-nodes=1 \
--max-nodes=10What happens behind the scenes: When pods can't be scheduled due to insufficient resources, Cluster Autoscaler adds nodes. When nodes are underutilized for 10 minutes, it drains and removes them.
Spot Instances (AWS) / Preemptible VMs (GCP)
Save 70-90% on compute costs for fault-tolerant workloads.
AWS EKS with Spot Instances:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: my-cluster
region: us-east-1
nodeGroups:
- name: spot-workers
instancesDistribution:
instanceTypes: ["t3.medium", "t3.large", "t3a.medium"]
onDemandBaseCapacity: 0
onDemandPercentageAboveBaseCapacity: 0 # 100% spot
spotInstancePools: 3
minSize: 2
maxSize: 20
labels:
workload-type: spot
taints:
- key: spot
value: "true"
effect: NoScheduleDeploy to Spot nodes:
spec:
tolerations:
- key: spot
operator: Equal
value: "true"
effect: NoSchedule
nodeSelector:
workload-type: spotBest practices for Spot:
- Use for stateless workloads (web servers, workers)
- Avoid for databases or stateful services
- Mix Spot and On-Demand nodes
- Implement graceful shutdown handlers (SIGTERM)
- Use multiple instance types for availability
Real mistake we've seen—and how to avoid it: Running critical production workloads entirely on Spot instances. Use 70% Spot / 30% On-Demand mix for resilience.
Namespace Resource Quotas
Prevent runaway costs by limiting resources per team/environment:
apiVersion: v1
kind: ResourceQuota
metadata:
name: dev-quota
namespace: development
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
persistentvolumeclaims: "10"
services.loadbalancers: "2"What this means for your infrastructure: Development namespace can't accidentally spin up 50 load balancers or consume all cluster resources.
Monitoring Costs
Kubecost: Open-source cost monitoring for Kubernetes
helm install kubecost kubecost/cost-analyzer \
--namespace kubecost --create-namespace \
--set kubecostToken="your-token"Access dashboard:
kubectl port-forward -n kubecost svc/kubecost-cost-analyzer 9090:9090Features:
- Per-namespace, deployment, pod cost breakdown
- Idle resource alerts
- Right-sizing recommendations
- Multi-cluster cost allocation
Cloud-native alternatives:
- AWS Cost Explorer: Filter by Kubernetes tags
- GCP Billing: GKE cost breakdown
- Azure Cost Management: AKS cost tracking
18. Production Readiness Checklist
Before going live, ensure you've addressed:
High Availability
- Multiple replicas (minimum 3 for critical services)
- Pod Disruption Budgets configured
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: my-app-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: my-app- Anti-affinity rules (spread pods across zones)
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: my-app
topologyKey: topology.kubernetes.io/zone- Multi-zone cluster (AWS: 3 AZs, GCP: 3 zones)
Security
- RBAC configured (no cluster-admin for users)
- Network policies enabled
- Secrets encrypted at rest (KMS integration)
- Pod Security Standards enforced (restricted level)
- Container images scanned (no HIGH/CRITICAL vulnerabilities)
- Non-root containers (runAsNonRoot: true)
- Read-only root filesystem where possible
- Resource limits set on all pods
Observability
- Metrics collection (Prometheus or cloud-native)
- Log aggregation (ELK, Loki, or CloudWatch)
- Distributed tracing (Jaeger, Zipkin, or cloud APM)
- Health check endpoints implemented
- Liveness and readiness probes configured
- Alerting rules defined (high error rate, pod crashes, resource exhaustion)
- SLO/SLI defined (target availability, latency)
Scalability
- Horizontal Pod Autoscaler configured
- Cluster Autoscaler enabled
- Resource requests/limits tuned
- Load testing completed (find breaking points)
Resilience
- Graceful shutdown implemented (handle SIGTERM)
- Circuit breakers for external dependencies
- Retry logic with exponential backoff
- Timeouts configured on all network calls
- Database connection pooling configured
Disaster Recovery
- Backup strategy defined (Velero, native snapshots)
- Recovery procedures documented
- Backup testing (restore from backup regularly)
- Multi-region failover (if required)
Cost Management
- Resource quotas per namespace
- Cost monitoring enabled (Kubecost, cloud billing)
- Spot instances for appropriate workloads
- Idle resource alerts configured
Documentation
- Architecture diagrams created
- Runbooks for common incidents
- On-call procedures documented
- Deployment procedures automated (GitOps)
Official Kubernetes production checklist: Production Best Practices
Final Recommendations from SimplifiedTechhub
Start Simple, Scale Complexity as Needed
Phase 1: Learning (Local Development)
- Use Minikube or Docker Desktop Kubernetes
- Deploy simple applications (single Deployment + Service)
- Focus on understanding core concepts
Phase 2: Non-Production (Staging/Dev)
- Deploy to managed Kubernetes (GKE, EKS, AKS)
- Add monitoring and logging
- Implement CI/CD pipelines
- Practice troubleshooting
Phase 3: Production (Critical Workloads)
- Implement full security hardening
- Add high availability and autoscaling
- Set up comprehensive monitoring and alerting
- Document everything
When to Get Expert Help
Consider SimplifiedTechhub's Premium Guidance when:
- Migrating critical production workloads (risk of downtime)
- Designing multi-cluster architectures (compliance, geo-distribution)
- Implementing service mesh (Istio, Linkerd complexity)
- Performance tuning (fixing bottlenecks, optimizing costs)
- Security hardening (compliance requirements, audit preparation)
- Disaster recovery planning (multi-region failover, backup strategies)
What you get:
- Architecture design review
- Hands-on implementation guidance
- Production readiness assessments
- Knowledge transfer to your team
- 24/7 support during critical migrations
Resources for Continued Learning
Official Documentation:
Interactive Learning:
Books:
- "Kubernetes Up and Running" by Kelsey Hightower
- "Kubernetes Patterns" by Bilgin Ibryam
- "Production Kubernetes" by Josh Rosso
Community:
Conclusion
Kubernetes is powerful but complex. The key to success is incremental adoption:
- Start with the basics: Understand Pods, Deployments, Services
- Build muscle memory: Deploy real applications to learn the patterns
- Add production concerns gradually: Security, monitoring, scaling
- Leverage managed services: Reduce operational burden
- Don't over-engineer: Use Kubernetes when it solves real problems
Remember: Kubernetes is a tool, not a goal. The objective is reliable, scalable applications—not Kubernetes itself.
Whether you're self-serving with these resources or working with SimplifiedTechhub's DevOps experts, you now have the foundation to deploy production-grade containerized applications with confidence.
Next steps:
- Deploy your first application following Scenario 1
- Set up local development with Skaffold or Tilt
- Implement health checks and monitoring
- Join the Kubernetes community
Need personalized guidance? SimplifiedTechhub's DevOps engineers are here to help you navigate complex migrations, optimize costs, and build resilient cloud-native infrastructure.
0 Comments