Back to Articles
Cloud Jan 21, 2026 · 16 min read

Kubernetes: A Beginner's Guide

Daniel

Software Developer

I avoided Kubernetes for two years. I thought it was overkill for small projects. Then I deployed an app that needed to handle traffic spikes, rolling updates, and auto-recovery. Docker Compose wasn’t enough anymore. Here’s everything I wish someone told me before I started.

Why Kubernetes Exists

Let me be blunt: if you’re deploying to a single server and don’t expect traffic spikes, you don’t need Kubernetes. Use Docker Compose. Seriously.

Kubernetes solves three specific problems at scale:

  1. Self-healing: If your app crashes at 3 AM, Kubernetes restarts it automatically. No PagerDuty wake-up call needed.
  2. Scaling: When Black Friday traffic hits, Kubernetes spins up more copies of your app. When traffic dies down, it scales back to save money.
  3. Zero-downtime deploys: Push new code without any user seeing an error page. Kubernetes rolls out updates gradually.

The catch: Kubernetes has a steep learning curve. The YAML files are intimidating, the concepts are abstract, and debugging feels like navigating a maze. But once it clicks, it’s incredibly powerful.

The Core Concepts (Simplified)

Forget the official documentation for a moment. Here’s how I explain Kubernetes to developers:

Pod: A Wrapper Around Your Container

A Pod is the smallest thing Kubernetes can manage. It’s a wrapper around one or more containers. In 99% of cases, a Pod contains exactly one container — your app.

# my-app-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-app
  labels:
    app: my-app
spec:
  containers:
    - name: my-app
      image: my-registry/my-app:1.0.0
      ports:
        - containerPort: 3000
      resources:
        requests:
          memory: "128Mi"
          cpu: "250m"
        limits:
          memory: "256Mi"
          cpu: "500m"

But here’s the thing: you almost never create Pods directly. You create Deployments, and the Deployment creates Pods for you. This is like how you don’t create EC2 instances manually — you use Auto Scaling Groups.

Deployment: Your Pod’s Manager

A Deployment tells Kubernetes: “I want 3 copies of my app running at all times. If one dies, replace it.”

# my-app-deployment.yaml
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: my-app
          image: my-registry/my-app:1.0.0
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: db-secret
                  key: url
          readinessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10

Notice the readinessProbe. This is crucial — Kubernetes won’t send traffic to your Pod until it’s ready. Without it, users might hit a Pod that’s still starting up.

Service: The Network Layer

Pods are ephemeral — they get created and destroyed constantly. You can’t rely on a Pod’s IP address. A Service gives your Pods a stable DNS name and load balances traffic across them.

# my-app-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app
spec:
  selector:
    app: my-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP

Now other apps in the cluster can reach your app at http://my-app (yes, Kubernetes has built-in DNS).

My First Real Deployment

Let me walk you through deploying an actual Node.js app to Kubernetes. I’ll use Minikube for local testing, but the same manifests work in production.

Step 1: Containerize Your App

Start with a good Dockerfile:

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

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

Step 2: Push to a Registry

# Build and tag
docker build -t my-registry/my-app:1.0.0 .

# Push to registry (Docker Hub, GCR, ECR, etc.)
docker push my-registry/my-app:1.0.0

Step 3: Deploy to Kubernetes

# Apply all manifests at once
kubectl apply -f k8s/

# Check if Pods are running
kubectl get pods
# NAME                    READY   STATUS    RESTARTS   AGE
# my-app-7d4b8c9f-x2k1m  1/1     Running   0          30s
# my-app-7d4b8c9f-y3n2p  1/1     Running   0          30s
# my-app-7d4b8c9f-z4q3r  1/1     Running   0          30s

# Check the Service
kubectl get svc my-app
# NAME     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
# my-app   ClusterIP   10.96.45.123   NONE          80/TCP    1m

Step 4: Test It

# Port-forward to access locally
kubectl port-forward svc/my-app 8080:80

# Now open http://localhost:8080 in your browser
# Your app is running on Kubernetes!

Debugging: The Survival Guide

When things go wrong (and they will), these commands save lives:

# Why isn't my Pod starting?
kubectl describe pod my-app-7d4b8c9f-x2k1m
# Look for "Events" section at the bottom

# What are the logs?
kubectl logs my-app-7d4b8c9f-x2k1m
# For previous container (if it crashed):
kubectl logs my-app-7d4b8c9f-x2k1m --previous

# Is my Service routing correctly?
kubectl get endpoints my-app
# Should show Pod IPs. If empty, labels don't match.

# Run a temporary debug pod
kubectl run debug --rm -it --image=alpine sh
# Inside the pod:
wget -qO- http://my-app
# If this works but your browser doesn't, it's an Ingress/Service issue

The 3 Things I Got Wrong

These mistakes cost me hours of debugging:

1. Resource Limits Are Not Optional

Without resource limits, a single Pod can eat all your node’s CPU and memory. I learned this the hard way — one Pod’s memory leak crashed every other Pod on the node.

# Always set resource requests AND limits
resources:
  requests:
    memory: "128Mi"  # Guaranteed minimum
    cpu: "250m"      # 0.25 CPU cores guaranteed
  limits:
    memory: "256Mi"  # Maximum before OOMKill
    cpu: "500m"      # Maximum before throttling

2. Liveness vs. Readiness Probes

Readiness probe: “Am I ready to receive traffic?” (Temporary failure = stop sending traffic)

Liveness probe: “Am I alive?” (Failure = restart the Pod)

Mixing these up is catastrophic. If your liveness probe is too aggressive, Kubernetes will kill and restart healthy Pods during high load (when the probe times out). I recommend starting with generous timeouts:

readinessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 5

3. Secrets Management

Never put secrets in your Deployment YAML. Use Kubernetes Secrets (or better, external secret managers like HashiCorp Vault or AWS Secrets Manager):

# Create a secret
kubectl create secret generic db-secret \
  --from-literal=url='postgres://user:pass@db:5432/mydb'

# Reference in your Deployment (see env section above)

When NOT to Use Kubernetes

I’ll say it again: Kubernetes is not always the answer.

  • Personal projects: Use Vercel, Railway, or Fly.io. Free tiers are generous.
  • Small teams (<5 devs): The operational overhead isn’t worth it. Use managed platforms.
  • Simple APIs: If your app is one container with no scaling needs, Docker Compose on a single VPS is simpler and cheaper.

Use Kubernetes when you have multiple services that need to communicate, need auto-scaling, or require zero-downtime deployments across multiple nodes.

Conclusion

Kubernetes is a powerful tool, but it’s not magic. Start simple — one Deployment, one Service. Get comfortable with kubectl commands. Then gradually add complexity as you need it. The best Kubernetes setup is the simplest one that meets your requirements.

The investment is worth it when you reach the point where you need it. Your 3 AM self will thank you when Kubernetes automatically restarts your crashed Pod and nobody notices.