DEV Community

Timevolt
Timevolt

Posted on

Kubernetes for Beginners: From Local to Production – A Quest Inspired by The Lord of the Rings

The Quest Begins (The "Why")

Honestly, I remember the first time I tried to take a simple Node.js app from my laptop to a real‑world server. I had Docker running locally, docker compose up worked like a charm, and I felt like a wizard. Then I pushed the image to a registry, SSH’d into a bare‑metal box, and tried docker run -p 8080:8080 myapp. The container started, but the moment I closed my SSH session the app vanished. Poof! Like a spell that only works while you’re chanting it.

I kept hitting the same wall: how do I make sure my service stays up, scales when traffic spikes, and can be rolled back without a midnight panic attack? I searched Stack Overflow, watched endless tutorials, and kept thinking there must be a better way—something that feels like the One Ring to rule them all, but for containers.

That’s when I stumbled onto Kubernetes. At first it looked like a dragon hoarding cryptic YAML, but once I understood the basics, it turned into the most powerful ally a developer could ask for. Let me take you through the journey, warts and all, so you can avoid the traps I fell into.

The Revelation (The Insight)

The “aha!” moment came when I stopped thinking of Kubernetes as a mystical black box and started seeing it as a desire‑state engine. You tell it what you want—I want three replicas of this image, exposed on port 80, with a rolling update strategy—and Kubernetes works relentlessly to make the cluster match that desire. If a pod dies, it spins up a new one. If you update the image, it rolls out the change pod by pod, keeping the service available.

What blew my mind was how declarative everything is. No more SSH‑ing into servers to tweak configs; you edit a YAML file, run kubectl apply -f file.yaml, and the system converges. It felt like when Neo finally sees the Matrix code—everything just clicked, and I realized I could describe my entire infrastructure as code, version it alongside my app, and treat it like any other piece of software.

Wielding the Power (Code & Examples)

Let’s walk through a tiny Go web server (you can swap it for Node, Python, whatever you like). First, the local struggle—the Docker‑only way that caused me grief.

Before: Docker‑only (the struggle)

# Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o main .

FROM alpine:3.20
WORKDIR /root/
COPY --from=builder /app/main .
EXPOSE 8080
CMD ["./main"]
Enter fullscreen mode Exit fullscreen mode
# Build & run locally
docker build -t myapp:latest .
docker run -d -p 8080:8080 --name myapp myapp:latest
Enter fullscreen mode Exit fullscreen mode

Works great on my laptop, but as soon as I log out the container stops (unless I add --restart=always, which still doesn’t give me scaling or zero‑downtime deploys).

After: Kubernetes manifests (the victory)

We’ll create two simple files: a Deployment (describes the pods) and a Service (exposes them).

deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp-deployment
  labels:
    app: myapp
spec:
  replicas: 3                     # <-- we want three instances
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:latest     # <-- push this image to a registry first
          ports:
            - containerPort: 8080
          # Common trap #1: forgetting imagePullPolicy
          imagePullPolicy: IfNotPresent   # use Always if you tag with :latest
          # Common trap #2: missing resource limits (can starve nodes)
          resources:
            requests:
              memory: "64Mi"
              cpu: "250m"
            limits:
              memory: "128Mi"
              cpu: "500m"
          # Common trap #3: exposing the wrong port inside the container
          # (make sure containerPort matches what your app listens on)
Enter fullscreen mode Exit fullscreen mode

service.yaml

apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
    - protocol: TCP
      port: 80          # port exposed inside the cluster
      targetPort: 8080  # forwards to containerPort
  type: LoadBalancer    # for cloud providers; use NodePort for local kind/minikube
Enter fullscreen mode Exit fullscreen mode

Now the spell casting:

# 1️⃣ Push your image to a registry you control (Docker Hub, GHCR, etc.)
docker tag myapp:latest <your-registry>/myapp:latest
docker push <your-registry>/myapp:latest

# 2️⃣ Apply the manifests
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml

# 3️⃣ Watch the magic happen
kubectl get deployments
kubectl get pods -w
kubectl get svc myapp-service
Enter fullscreen mode Exit fullscreen mode

If you’re using a local cluster like Kind or Minikube, expose the service with:

kubectl port-forward svc/myapp-service 8080:80
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:8080 and you’ll see your app, now running three replicas, self‑healing, and ready for a rolling update.

The Traps (mistakes I made)

  1. ImagePullPolicy set to Never – Kubernetes never pulled my updated image, so I kept running old code. Fix: use IfNotPresent for local dev or Always when you push :latest.
  2. No resource requests/limits – The scheduler placed pods on nodes that eventually ran out of memory, causing OOM kills. Adding modest requests/limits saved my cluster.
  3. Mismatched ports – I set containerPort: 80 while my app listened on 8080, leading to a service that appeared healthy but returned connection refused. Double‑check the port your container actually binds to.

Fix those, and your deployments go from “why is this down?” to “nice, it just works”.

Why This New Power Matters

With Kubernetes in your toolkit, you stop being a server‑admin firefighter and start being a product‑focused developer. You can:

  • Scale instantly – change replicas: 3 to replicas: 10 and watch the cluster spin up more pods.
  • Update safely – roll out a new image with kubectl set image deployment/myapp-deployment myapp=<your-registry>/myapp:v2 and Kubernetes does a rolling update, pausing if health checks fail.
  • Self‑heal – kill a pod manually (kubectl delete pod <pod-name>) and a new one appears in seconds.
  • Deploy anywhere – the same manifests work on a local Kind cluster, a managed EKS/GKE/AKS cluster, or even on‑prem bare metal.

It’s like gaining the ability to cast Protego on your entire infrastructure—your apps stay up, your users stay happy, and you get to sleep through the night.

Your Turn: The Next Quest

Ready to take the plunge? Here’s a fun challenge: take the tiny app you just containerized, push it to a registry, and deploy it to a free tier Kubernetes playground (like Katacoda, Play with Kubernetes, or a GitHub Codespaces + Kind setup). Try changing the replica count, updating the image, and watching the rollback with kubectl rollout undo.

What’s the first thing you’ll tweak when you see your app running three pods at once? Drop a comment below—I’d love to hear about your own Kubernetes adventures! 🚀

Top comments (0)