Skip to main content

🚒 Deploying Applications on Kubernetes

Knowing the objects is one thing; shipping a real app safely is another. This lesson is the practical payoff: how to containerize well, package your manifests, roll out new versions without downtime, autoscale under load, and see what's happening in production.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Build a production-grade container image with multi-stage builds and health checks
  • Choose between raw manifests, Helm, and Kustomize for packaging
  • Explain and apply rolling, blue-green, and canary deployment strategies
  • Autoscale workloads with the Horizontal Pod Autoscaler and cluster autoscaler
  • Add observability β€” probes, metrics, logs, and traces β€” to a deployment

Estimated Time: 45–55 minutes  β€’  Difficulty: Intermediate–Advanced

Hands-on: Deploy a three-tier app with health checks, autoscaling, and an ingress.

In This Lesson

From Code to Cluster

Deploying to Kubernetes is a pipeline: application code becomes a container image, the image plus manifests describe your desired state, and a CI/CD pipeline applies them to the cluster β€” then you maintain, scale, and observe what's running.

flowchart LR A[App Code] --> B[Container Image] B --> C[K8s Manifests] C --> D[Running App] F[CI/CD Pipeline] --> B F --> C D --> E[Scale Β· Update Β· Observe] E -->|new version| A

Each stage has decisions to make well. We'll walk them in order: build the image right, package the manifests, roll out safely, scale automatically, and watch it all.

Containerizing for Kubernetes

A good Kubernetes deployment starts with a good image. Follow these principles: one concern per container, run as a non-root user, handle SIGTERM for graceful shutdown, log to stdout/stderr, and keep the image small with a multi-stage build.

# Stage 1 β€” build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 2 β€” lean runtime image
FROM node:20-alpine
# Create and use a non-root user
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
USER app
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/server.js"]

πŸ“– Why multi-stage?

The build stage carries compilers, dev dependencies, and source β€” hundreds of megabytes you never want in production. The final stage copies out only the built artifacts, producing a smaller, faster-pulling, lower-attack-surface image.

Configuration from the environment

Per the Twelve-Factor App, read config from environment variables so the same image runs unchanged across dev, staging, and prod. Validate required values on startup so misconfiguration fails loudly and early:

const config = {
  port: process.env.PORT || 3000,
  dbHost: process.env.DB_HOST || 'localhost',
  dbUser: process.env.DB_USER,
  dbPassword: process.env.DB_PASSWORD,
  logLevel: process.env.LOG_LEVEL || 'info',
};

const required = ['DB_USER', 'DB_PASSWORD'];
const missing = required.filter((key) => !process.env[key]);
if (missing.length > 0) {
  throw new Error(`Missing required env vars: ${missing.join(', ')}`);
}

export default config;

Health-check endpoints

Kubernetes probes need endpoints to hit. Expose a lightweight liveness check and a readiness check that verifies real dependencies:

import express from 'express';
const app = express();

// Liveness: is the process alive?
app.get('/health', (req, res) => res.status(200).send('OK'));

// Readiness: are dependencies reachable?
app.get('/ready', async (req, res) => {
  try {
    await db.ping();
    await redis.ping();
    res.status(200).send('Ready');
  } catch (err) {
    res.status(503).send('Not Ready');
  }
});

Manifests, Helm & Kustomize

You have three main ways to package the YAML you apply to a cluster. Choose by how much environment variation and reuse you need.

1. Raw manifests

Plain YAML applied with kubectl apply -f. Maximum clarity and control β€” perfect for learning and small apps, but repetitive once you have several environments.

# Imperative (quick, one-off β€” avoid for production)
kubectl create deployment web --image=nginx:1.27

# Declarative (version-controlled, repeatable β€” preferred)
kubectl apply -f k8s/

2. Helm β€” the package manager

Helm packages manifests as reusable, templated charts with a values.yaml for configuration, plus release management and one-command rollbacks.

graph TD A[Helm Chart Templates] --> C[Rendered Manifests] B[values.yaml] --> C C --> D[Kubernetes API]
# Install a public chart
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-nginx bitnami/nginx

# Install your own chart with custom values, then upgrade
helm install my-app ./chart --values prod-values.yaml
helm upgrade my-app ./chart --values prod-values.yaml
helm rollback my-app 1   # instant rollback to revision 1

3. Kustomize β€” overlays without templating

Built into kubectl, Kustomize keeps a base set of manifests and applies environment-specific overlays as patches β€” no templating language required.

graph TD A[base/] --> D[Kustomize] B[overlays/dev] --> D C[overlays/prod] --> D D --> E[Final Manifests]
kubectl apply -k overlays/production/
ApproachBest when
Raw manifestsLearning, small apps, one environment
HelmReusable/shareable packages, many parameters, release management
KustomizeSame app across dev/staging/prod with small differences

Deployment Strategies

How you replace the old version with the new one is a real engineering choice, trading resources against safety.

Rolling update (the default)

Kubernetes gradually replaces old pods with new ones, a few at a time, keeping the service up throughout. Tune it with maxSurge (how many extra pods may be created) and maxUnavailable (how many may be down).

flowchart LR A[3Γ— v1] --> B[2Γ— v1, 1Γ— v2] --> C[1Γ— v1, 2Γ— v2] --> D[3Γ— v2]
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0   # never drop below full capacity

Pros: zero downtime, no extra long-term resources. Cons: both versions run at once during rollout; rollback is gradual, not instant.

Blue-green

Run the new version ("green") fully alongside the old ("blue"), test it, then flip the Service selector to switch all traffic at once. Rollback is instant β€” flip back.

# Switch traffic by changing one label on the Service
apiVersion: v1
kind: Service
metadata:
  name: web-app
spec:
  selector:
    app: web-app
    version: blue   # change to "green" to cut over
  ports:
    - port: 80
      targetPort: 8080

Pros: instant cutover and rollback, full pre-test. Cons: needs double the resources during the switch.

Canary

Send a small slice of traffic (say 10%) to the new version, watch metrics, then ramp up if healthy or pull it if not. In its simplest form you run a small canary Deployment beside the stable one behind the same Service; service meshes (Istio, Linkerd) or Argo Rollouts give you precise percentage control.

flowchart TD A[Service] --> B[90% β†’ stable v1] A --> C[10% β†’ canary v2] C --> D{Healthy?} D -->|Yes| E[Ramp to 100%] D -->|No| F[Roll back]

Pros: tests with real traffic, limits blast radius. Cons: more moving parts; needs good monitoring to judge "healthy."

Scaling & Autoscaling

You can scale by hand, but the whole point of orchestration is to let the system do it.

# Manual scale
kubectl scale deployment web-app --replicas=5

Horizontal Pod Autoscaler (HPA)

The HPA adds and removes pod replicas automatically based on metrics like CPU utilization or custom application metrics. It's the everyday autoscaler.

graph TD A[Metrics Server] --> B[HPA] B -->|adjusts replicas| C[Deployment] C --> D[Pods]
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

Two companions round out scaling: the Vertical Pod Autoscaler right-sizes CPU/memory requests for a pod, and the Cluster Autoscaler adds or removes nodes when pods can't be scheduled or nodes sit idle. HPA scales pods; Cluster Autoscaler scales the machines under them.

⚠️ HPA needs resource requests

CPU-based autoscaling is a percentage of the pod's CPU request. If you don't set resources.requests.cpu, the HPA has no baseline to compute utilization against and won't work. Always declare requests.

Observability

You can't operate what you can't see. Production observability rests on three pillars β€” metrics, logs, and traces β€” plus the health probes that let Kubernetes act on its own.

graph TD A[Observability] --> B[Metrics: Prometheus + Grafana] A --> C[Logs: Fluent Bit + Loki/ELK] A --> D[Traces: OpenTelemetry + Jaeger] A --> E[Alerts: Alertmanager]
  • Metrics β€” Prometheus scrapes numeric time-series (request rate, latency, CPU); Grafana visualizes them.
  • Logs β€” a node agent (Fluent Bit) ships container stdout/stderr to a store (Loki or Elasticsearch) you can search.
  • Traces β€” OpenTelemetry follows a single request across many services so you can find the slow hop.
  • Alerts β€” Alertmanager routes threshold breaches to Slack, email, or PagerDuty.

Configure all three probes

Probes are the most immediate form of observability β€” they let Kubernetes detect and fix problems automatically:

    spec:
      containers:
        - name: web-app
          image: my-registry/web-app:1.0.0
          ports:
            - containerPort: 80
          startupProbe:        # give slow starts time before liveness kicks in
            httpGet:
              path: /startup
              port: 80
            periodSeconds: 2
            failureThreshold: 30
          livenessProbe:       # restart if deadlocked
            httpGet:
              path: /health
              port: 80
            periodSeconds: 10
            failureThreshold: 3
          readinessProbe:      # remove from load balancing until ready
            httpGet:
              path: /ready
              port: 80
            periodSeconds: 5

Hands-on Exercise

πŸ‹οΈ Deploy a Three-Tier App

Scenario: A React frontend, a Node.js API, and a MongoDB database, all in one namespace, with best practices applied.

Your tasks:

  1. Create a namespace for the app.
  2. Deploy MongoDB as a StatefulSet with a PVC; expose it with a headless Service.
  3. Deploy the API and frontend as Deployments with resource requests/limits and health probes.
  4. Add an HPA on the API (CPU 70%, 2–10 replicas) and an Ingress routing / to the frontend and /api to the API.
πŸ’‘ Hint

Feed the API its Mongo credentials from a Secret and its non-sensitive settings from a ConfigMap. The Mongo Service should be headless (clusterIP: None) because it backs a StatefulSet. Remember the HPA only works because the API's pods declare CPU requests.

βœ… Solution outline (key pieces)
apiVersion: v1
kind: Namespace
metadata:
  name: three-tier-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: three-tier-app
spec:
  replicas: 2
  selector:
    matchLabels: { app: backend }
  template:
    metadata:
      labels: { app: backend }
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "3000"
    spec:
      containers:
        - name: backend
          image: my-registry/backend:1.0.0
          ports:
            - containerPort: 3000
          env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef: { name: mongodb-secret, key: password }
          resources:
            requests: { cpu: "200m", memory: "256Mi" }
            limits:   { cpu: "500m", memory: "512Mi" }
          readinessProbe:
            httpGet: { path: /ready, port: 3000 }
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: backend-hpa
  namespace: three-tier-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: backend
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target: { type: Utilization, averageUtilization: 70 }

Add the frontend Deployment/Service, the MongoDB StatefulSet + headless Service + PVC, and an Ingress with two paths, then kubectl apply -f the directory.

🎯 Quick Quiz

Question 1: Which strategy switches all traffic at once and offers instant rollback, at the cost of double the resources?

Question 2: Why must you set CPU resource requests for a CPU-based HPA to function?

Question 3: Which packaging tool uses a base plus environment overlays and is built into kubectl?

Best Practices

βœ… Do

  • Always set resource requests and limits β€” they drive scheduling and autoscaling
  • Run multiple replicas and add pod anti-affinity and a PodDisruptionBudget for real availability
  • Store manifests in Git and deploy via CI/CD and GitOps (Argo CD, Flux)
  • Run containers as non-root, scan images for vulnerabilities, and apply network policies
  • Configure all three probes and export metrics from day one

⚠️ Don't

  • Deploy imperatively to production β€” it isn't reproducible or reviewable
  • Use the :latest image tag β€” pin explicit versions so rollouts and rollbacks are deterministic
  • Skip readiness probes β€” without them, traffic hits pods that aren't ready and users see errors
  • Run a database in-cluster without understanding its storage, backups, and failover first

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Great deployments start with a great image: multi-stage builds, non-root, health endpoints, env-based config.
  • Package with raw manifests, Helm, or Kustomize depending on reuse and environment variation.
  • Rolling, blue-green, and canary trade resources against rollout safety β€” pick per risk.
  • The HPA scales pods on metrics (and needs resource requests); the Cluster Autoscaler scales nodes.
  • Observability β€” probes, metrics, logs, traces β€” is what makes production operable, not optional.

πŸ“š Further Reading

πŸš€ What's Next?

Your app is deployed, scaling, and rolling out safely. The last piece is keeping it healthy over time. Next up: Application Monitoring Strategies β€” turning the observability signals we introduced here into dashboards, alerts, and on-call practices.

πŸŽ‰ You can ship on Kubernetes!

Containerize, package, roll out, autoscale, observe β€” you've closed the loop.