DEV Community

Cover image for Kubernetes for DevOps Engineers: Pods, Deployments, Volumes, and a Real Next.js Deployment Guide
Hamza Amir
Hamza Amir

Posted on

Kubernetes for DevOps Engineers: Pods, Deployments, Volumes, and a Real Next.js Deployment Guide

What Is Kubernetes

Kubernetes is a container orchestration platform. Google built the first version and later donated the project to the Cloud Native Computing Foundation. Teams use Kubernetes to run, scale, and manage containerized applications across many machines.

A container packages your app with everything it needs to run: code, runtime, libraries, and settings. Docker made containers easy to build and run on a single machine. But production systems rarely run on one machine. You need many containers, spread across many servers, staying healthy around the clock. That is the job Kubernetes does.

Kubernetes takes a cluster of machines and turns them into one large, programmable system. You describe the state you want, for example "run five copies of this app," and Kubernetes keeps that state running. If a container crashes, Kubernetes restarts it. If a server dies, Kubernetes moves the workload to a healthy server. You stop managing individual containers by hand and start managing desired outcomes.

Docker vs Kubernetes: Different Jobs

Comparing Docker and Kubernetes as if they compete misses the point. They solve different problems.

Docker builds container images and runs them on a single host. Docker Engine handles image creation, container startup, networking on one machine, and local storage.

Kubernetes manages containers across a fleet of machines. Kubernetes does not build images. Kubernetes schedules containers onto servers, restarts failed containers, scales apps up and down, routes network traffic, and rolls out updates without downtime.

Here is a direct comparison:

Task Docker Kubernetes
Build a container image Yes No
Run a container on one machine Yes Yes, across many machines
Restart a crashed container automatically Limited Built in
Scale to 50 replicas on demand Manual scripting One command
Load balance traffic across replicas Manual setup Built in (Services)
Roll out a new version with zero downtime Manual scripting Built in (rolling updates)
Self-heal after a server failure No Yes
Manage secrets and configuration at scale Basic Built in (ConfigMaps, Secrets)

Most production setups use both. Docker builds the image. Kubernetes runs that image reliably at scale. A pro DevOps engineer does not choose one over the other. The engineer picks Kubernetes when the app needs to survive failures, scale automatically, and deploy safely across a cluster, not just a laptop or single server.

Why Kubernetes Wins for Production Workloads

Plain Docker on a single host has real limits once traffic grows or uptime matters.

  • No automatic failover. If the host machine goes down, every container on it goes down with no automatic recovery.
  • No built-in scaling logic. You write your own scripts to add or remove containers based on load.
  • No native load balancing across multiple hosts.
  • No rolling updates. A bad deployment can take your app offline until you fix and redeploy manually.
  • No declarative state management. You track what should be running yourself, often through custom scripts.

Kubernetes solves each of these directly:

  • Self-healing: Kubernetes detects failed containers and replaces them automatically.
  • Horizontal scaling: you set a target replica count, or let the Horizontal Pod Autoscaler adjust replicas based on CPU or custom metrics.
  • Built-in load balancing: Services distribute traffic across every healthy replica.
  • Rolling updates and rollbacks: Kubernetes updates your app one replica at a time and rolls back automatically if health checks fail.
  • Declarative configuration: you write YAML files describing the desired state, and Kubernetes keeps the cluster matching that state.

This is the real reason engineering teams adopt Kubernetes. Uptime, recovery speed, and scaling all become automated instead of manual.

Kubernetes Architecture at a Glance

A Kubernetes cluster has two main parts.

Control plane
The control plane makes decisions for the cluster. Key components:

  • API server: the front door for every command you send to the cluster.
  • etcd: the database that stores the cluster's current state.
  • Scheduler: decides which node runs each new Pod.
  • Controller manager: watches the cluster and works to match the actual state to the desired state.

Worker nodes
Worker nodes run your actual application containers. Key components:

  • Kubelet: the agent that talks to the control plane and manages containers on that node.
  • Container runtime: the software that runs containers, such as containerd.
  • Kube-proxy: handles network rules so traffic reaches the right Pod.

You send commands to the API server, usually through the kubectl command line tool. The control plane then works with the worker nodes to make your request real.

Types of clusters you will run into

  • Managed cloud clusters: Google GKE, Amazon EKS, and Azure AKS run the control plane for you. You manage worker nodes and workloads only.
  • Self-managed clusters: tools like kubeadm let you run every control plane component yourself, on your own servers.
  • Local development clusters: Minikube, kind, and k3d spin up a small cluster on your laptop for testing before you push to a real environment.

A cluster can run on three nodes or three thousand nodes. The same YAML files, the same kubectl commands, and the same Deployment and Service objects work at both sizes. That consistency is a core reason teams standardize on Kubernetes across every environment.

Pods Explained

A Pod is the smallest deployable unit in Kubernetes. A Pod wraps one or more containers that share the same network address and storage.

Most Pods run a single container. Kubernetes still uses the Pod as the base unit because some apps need a main container plus a helper container, called a sidecar, sharing the same network namespace. Examples include a logging agent or a proxy running next to your app container.

Pods are temporary. Kubernetes creates and destroys Pods constantly during scaling, updates, and failure recovery. You rarely create standalone Pods in production. Instead, you manage Pods through higher-level objects like Deployments.

A minimal Pod definition looks like this:

apiVersion: v1
kind: Pod
metadata:
  name: my-app-pod
spec:
  containers:
    - name: my-app
      image: my-app:1.0
      ports:
        - containerPort: 3000
Enter fullscreen mode Exit fullscreen mode

ReplicaSets and Deployments

A ReplicaSet keeps a specified number of identical Pods running at all times. If a Pod dies, the ReplicaSet creates a replacement.

You rarely write a ReplicaSet directly. You use a Deployment instead. A Deployment manages ReplicaSets for you and adds rolling updates, rollbacks, and version history on top.

Here is a Deployment for a Node.js app running three replicas:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: my-app:1.0
          ports:
            - containerPort: 3000
Enter fullscreen mode Exit fullscreen mode

Apply this file with:

kubectl apply -f deployment.yaml
Enter fullscreen mode Exit fullscreen mode

Kubernetes creates three Pods, spreads them across available nodes, and keeps three running at all times. Update the image tag and reapply the file, and Kubernetes rolls out the new version one Pod at a time, checking health along the way.

Services Explained

Pods get a new internal IP address every time Kubernetes recreates them. Your frontend cannot reliably connect to a backend if the backend's address keeps changing. A Service solves this.

A Service gives a stable network address and DNS name to a group of Pods, selected by label. Traffic sent to the Service gets distributed across every matching, healthy Pod.

apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 3000
  type: ClusterIP
Enter fullscreen mode Exit fullscreen mode

Common Service types:

  • ClusterIP: exposes the Service inside the cluster only. Default type.
  • NodePort: exposes the Service on a static port on every node, reachable from outside the cluster.
  • LoadBalancer: provisions an external load balancer through your cloud provider.
  • ExternalName: maps the Service to an external DNS name.

Deployments keep your Pods running. Services keep traffic flowing to them, no matter which Pods come and go.

Namespaces, ConfigMaps, and Secrets

Three more building blocks come up in daily Kubernetes work.

  • Namespaces split one cluster into isolated sections. Teams commonly run separate namespaces for development, staging, and production.
  • ConfigMaps store non-sensitive configuration data, like environment variables or app settings, separately from your container image.
  • Secrets store sensitive data, like API keys and database passwords, in a format Kubernetes handles with tighter access controls than a ConfigMap.

Storing configuration outside your image means you rebuild the image less often and swap settings per environment without touching code.

Volumes: Persistent Storage for Pods

A container's filesystem disappears the moment that container stops. Fine for a stateless web server. A problem for a database or any app that writes files you need to keep.

Kubernetes solves this with Volumes.

Volume
A Volume attaches storage to a Pod. The storage lives as long as the Pod does, and every container in that Pod can read and write to it. A basic emptyDir Volume gives containers in the same Pod a shared scratch space that survives container restarts but not Pod deletion.

PersistentVolume (PV)
A PersistentVolume is a piece of storage provisioned at the cluster level, separate from any single Pod. Your platform team, or your cloud provider through dynamic provisioning, sets up PVs backed by real disks, network storage, or cloud storage services.

PersistentVolumeClaim (PVC)
A PersistentVolumeClaim is a request for storage from a Pod. You define how much space you need and what access mode you need, and Kubernetes binds your claim to a matching PersistentVolume. The Pod mounts the PVC, not the raw storage directly.

StorageClass
A StorageClass defines a category of storage, for example fast SSD storage versus standard disk storage. Set a StorageClass on your PVC, and Kubernetes provisions the right kind of disk automatically instead of waiting for a cluster admin to create one by hand.

Here is a PVC requesting 5Gi of storage, followed by a Pod that mounts it:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data-claim
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard
Enter fullscreen mode Exit fullscreen mode
apiVersion: v1
kind: Pod
metadata:
  name: app-with-storage
spec:
  containers:
    - name: my-app
      image: my-app:1.0
      volumeMounts:
        - mountPath: /data
          name: app-storage
  volumes:
    - name: app-storage
      persistentVolumeClaim:
        claimName: app-data-claim
Enter fullscreen mode Exit fullscreen mode

The Pod above writes to /data, and that data stays intact across Pod restarts and rescheduling. Stateless apps like a Next.js frontend rarely need this. Databases, file uploads, and caches usually do.

A Complete Example, Start to Finish

Here is the full picture for deploying a simple app:

  1. Build your Docker image and push it to a registry.
  2. Write a Deployment YAML file setting your image, replica count, and container port.
  3. Write a Service YAML file to expose that Deployment.
  4. Apply both files with kubectl apply -f.
  5. Check status with kubectl get pods and kubectl get services.
  6. Update the image tag in the Deployment file and reapply it to ship a new version with zero downtime.

That workflow replaces manual server setup, manual restarts, and manual traffic routing with a few YAML files and one command line tool.

Deploying a Next.js App to Kubernetes, Step by Step

Here is the full path from a working Next.js app to a live app running on Kubernetes.

Step 1: Set your Next.js output mode

Open next.config.js and set standalone output. This trims your production build down to only the files your app needs at runtime.

// next.config.js
module.exports = {
  output: "standalone",
};
Enter fullscreen mode Exit fullscreen mode

Step 2: Write a Dockerfile

# Stage 1: install dependencies and build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2: run the production build
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

This two-stage build keeps your final image small. The builder stage installs dependencies and compiles your app. The runner stage copies over only the standalone output.

Step 3: Build and push your image

docker build -t myregistry/nextjs-app:1.0 .
docker push myregistry/nextjs-app:1.0
Enter fullscreen mode Exit fullscreen mode

Replace myregistry with your actual registry, for example Docker Hub, GitHub Container Registry, or your cloud provider's registry.

Step 4: Write your Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nextjs-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nextjs-app
  template:
    metadata:
      labels:
        app: nextjs-app
    spec:
      containers:
        - name: nextjs-app
          image: myregistry/nextjs-app:1.0
          ports:
            - containerPort: 3000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
Enter fullscreen mode Exit fullscreen mode

Step 5: Write your Service

apiVersion: v1
kind: Service
metadata:
  name: nextjs-app-service
spec:
  type: LoadBalancer
  selector:
    app: nextjs-app
  ports:
    - port: 80
      targetPort: 3000
Enter fullscreen mode Exit fullscreen mode

Step 6: Apply both files

kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
Enter fullscreen mode Exit fullscreen mode

Step 7: Check that your app is running

kubectl get pods
kubectl get service nextjs-app-service
Enter fullscreen mode Exit fullscreen mode

The second command returns an external IP address once your cloud provider finishes provisioning the load balancer. Open that address in a browser, and your Next.js app loads.

Step 8: Ship a new version

Build a new image with a new tag, push it, then update the Deployment:

docker build -t myregistry/nextjs-app:1.1 .
docker push myregistry/nextjs-app:1.1
kubectl set image deployment/nextjs-app nextjs-app=myregistry/nextjs-app:1.1
Enter fullscreen mode Exit fullscreen mode

Kubernetes rolls out the new image one Pod at a time. Your users stay connected the whole time, since the Service only routes traffic to Pods that pass their health check.

When You Should Use Kubernetes

Kubernetes adds real value once you need more than one server, automatic recovery from failure, and safe rolling deployments. A single small app on a single server may not need this complexity yet.

Reach for Kubernetes when your app needs to:

  • Scale beyond what one machine handles.
  • Stay online through hardware failures.
  • Deploy updates without downtime.
  • Run consistently across development, staging, and production.
  • Manage many microservices instead of one monolith.

If your project is a side project on one small server, plain Docker or Docker Compose may serve you better for now. Kubernetes pays off at production scale.

Top comments (0)