LLMs  

Running Applications on Kubernetes: Production Patterns Beyond Basic Deployments

Deploying an application to Kubernetes is relatively straightforward. Running that application reliably in a production environment requires much more thought.

After working with CI/CD pipelines and Infrastructure as Code, I started looking more closely at what happens after an application reaches a Kubernetes cluster.

Creating a Deployment and exposing it through a Service is only the beginning. Production workloads need health checks, resource controls, scaling, configuration management, controlled updates and a clear recovery strategy.

In this article, I will explore some of the Kubernetes patterns I use to move from a basic deployment towards a more production-ready application architecture.

Production Goal

Application

Kubernetes Deployment

Health Checks + Resource Controls + Scaling

Controlled Rollout

Reliable Production Service

Starting with a Basic Kubernetes Deployment

A simple Kubernetes Deployment may look like this:

apiVersion: apps/v1
kind: Deployment

metadata:
  name: cloud-native-api

spec:
  replicas: 2

  selector:
    matchLabels:
      app: cloud-native-api

  template:
    metadata:
      labels:
        app: cloud-native-api

    spec:
      containers:
        - name: cloud-native-api
          image: myregistry/cloud-native-api:1.0
          ports:
            - containerPort: 8080

This runs two replicas of the application.

For a development environment, this may be enough to get started. In production, however, Kubernetes needs more information about how the application behaves.

Adding Readiness and Liveness Probes

One of the first improvements I make is adding health probes.

Kubernetes should understand the difference between an application that is running and an application that is ready to receive traffic.

readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080

  initialDelaySeconds: 5
  periodSeconds: 10
  timeoutSeconds: 2
  failureThreshold: 3

livenessProbe:
  httpGet:
    path: /health/live
    port: 8080

  initialDelaySeconds: 15
  periodSeconds: 20
  timeoutSeconds: 2
  failureThreshold: 3

Readiness Probe

A readiness probe determines whether the application should receive traffic.

A pod may be running but still loading configuration, connecting to dependencies or preparing internal services.

Until the readiness check succeeds, Kubernetes can keep that pod out of Service endpoints.

Liveness Probe

A liveness probe is used to determine whether the application is still functioning.

If the application becomes unhealthy for long enough, Kubernetes can restart the container.

Important: readiness and liveness checks should represent different conditions where possible. A temporary dependency issue should not automatically cause the application container to be restarted unnecessarily.

Using Startup Probes for Slow Starting Applications

Some applications need more time during startup.

If a liveness probe begins too early, Kubernetes may restart the container before the application has completed its startup process.

A startup probe can be used for this scenario.

startupProbe:
  httpGet:
    path: /health/live
    port: 8080

  periodSeconds: 5
  failureThreshold: 30

In this example, Kubernetes allows the application additional time to start before liveness checking becomes active.

Defining Resource Requests and Limits

Another important production consideration is resource management.

Without requests and limits, Kubernetes has less information about how much CPU and memory an application requires.

resources:
  requests:
    cpu: "200m"
    memory: "256Mi"

  limits:
    cpu: "500m"
    memory: "512Mi"

Resource Requests

Requests help the Kubernetes scheduler understand the minimum resources that should be available for the container.

Resource Limits

Limits define the maximum resource usage allowed for the container.

These values should not be guessed permanently. They should be adjusted using monitoring and real workload behaviour.

My approach: I prefer starting with measured values from testing or observability data and then adjusting resource requests and limits based on how the application actually behaves.

Managing Application Configuration

Application configuration should normally be separated from the container image.

Kubernetes ConfigMaps can be used for non-sensitive configuration.

apiVersion: v1
kind: ConfigMap

metadata:
  name: cloud-native-api-config

data:
  ASPNETCORE_ENVIRONMENT: "Production"
  LOG_LEVEL: "Information"

The configuration can then be injected into the container:

envFrom:
  - configMapRef:
      name: cloud-native-api-config

This makes it possible to change environment configuration without creating a completely new application image for every setting.

Handling Sensitive Configuration

Sensitive values such as passwords, tokens and certificates should not be stored directly inside application manifests committed to source control.

Kubernetes Secrets can provide a basic mechanism for injecting sensitive values.

env:
  - name: DATABASE_PASSWORD

    valueFrom:
      secretKeyRef:
        name: database-secret
        key: password

In larger environments, I would normally integrate Kubernetes with a dedicated secrets management platform rather than relying only on manually created Kubernetes Secrets.

Access controls around secrets are just as important as the mechanism used to store them.

Using Rolling Updates

Kubernetes Deployments support rolling updates.

Instead of stopping every existing pod and then starting the new version, Kubernetes can gradually replace old replicas.

strategy:
  type: RollingUpdate

  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

With this configuration, Kubernetes can create an additional pod before removing an existing one.

Version 1 Pods

Start Version 2 Pod

Readiness Check Passes

Remove Version 1 Pod

Continue Until Rollout Completes

Readiness probes become especially important during rolling deployments because Kubernetes should only route traffic to new pods once they are actually ready.

Scaling Applications Horizontally

Applications do not always receive the same level of traffic.

Kubernetes can increase or decrease the number of replicas based on resource utilisation or other metrics.

A simple HorizontalPodAutoscaler could look like this:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler

metadata:
  name: cloud-native-api

spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: cloud-native-api

  minReplicas: 2
  maxReplicas: 10

  metrics:
    - type: Resource

      resource:
        name: cpu

        target:
          type: Utilization
          averageUtilization: 70

In this example, Kubernetes can scale the Deployment between two and ten replicas.

CPU utilisation is used as the scaling signal.

Normal Traffic

2 Pods

Increased Load

CPU Threshold Reached

Kubernetes Adds More Pods

In more advanced environments, scaling can also be based on application metrics, queue depth or other workload-specific signals.

Distributing Pods Across Nodes

Running multiple replicas does not automatically guarantee high availability if every pod ends up on the same Kubernetes node.

Pod anti-affinity or topology spread constraints can help distribute replicas across different nodes or zones.

A simple topology spread configuration might look like this:

topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: kubernetes.io/hostname
    whenUnsatisfiable: ScheduleAnyway

    labelSelector:
      matchLabels:
        app: cloud-native-api

This encourages Kubernetes to distribute matching pods across different worker nodes.

Kubernetes Cluster

Node 1 → API Pod
Node 2 → API Pod
Node 3 → Additional Capacity

Using a Pod Disruption Budget

Maintenance operations can sometimes cause multiple pods to become unavailable.

A PodDisruptionBudget helps define how much voluntary disruption an application can tolerate.

apiVersion: policy/v1
kind: PodDisruptionBudget

metadata:
  name: cloud-native-api-pdb

spec:
  minAvailable: 1

  selector:
    matchLabels:
      app: cloud-native-api

This example requests that at least one matching pod remains available during voluntary disruptions.

Graceful Application Shutdown

During a deployment or scaling event, Kubernetes may terminate a pod.

Applications should be given enough time to complete active requests before shutting down.

terminationGracePeriodSeconds: 30

The application should also respond correctly to termination signals so that it can stop accepting new work and complete existing requests where possible.

This becomes particularly important for APIs that process longer-running operations.

Using Immutable Image Versions

I avoid relying exclusively on the latest tag for production deployments.

Instead of:

myregistry/cloud-native-api:latest

I prefer versioned images such as:

myregistry/cloud-native-api:1.0.0
myregistry/cloud-native-api:1.0.1
myregistry/cloud-native-api:1.1.0

Another option is using Git commit identifiers.

myregistry/cloud-native-api:a53fd21

This makes it easier to identify exactly which application build is running and simplifies rollback.

Rolling Back a Failed Deployment

Even a well-tested release can fail after deployment.

Kubernetes keeps Deployment rollout history that can help with recovery.

kubectl rollout history deployment/cloud-native-api

A previous revision can be restored using:

kubectl rollout undo deployment/cloud-native-api

I prefer rollback procedures to be documented and tested before a production incident happens.

Putting Everything Together

A more production-focused Kubernetes Deployment can combine several of these patterns.

apiVersion: apps/v1
kind: Deployment

metadata:
  name: cloud-native-api

spec:
  replicas: 2

  strategy:
    type: RollingUpdate

    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1

  selector:
    matchLabels:
      app: cloud-native-api

  template:
    metadata:
      labels:
        app: cloud-native-api

    spec:
      terminationGracePeriodSeconds: 30

      containers:
        - name: cloud-native-api
          image: myregistry/cloud-native-api:1.0.0

          ports:
            - containerPort: 8080

          envFrom:
            - configMapRef:
                name: cloud-native-api-config

          resources:
            requests:
              cpu: "200m"
              memory: "256Mi"

            limits:
              cpu: "500m"
              memory: "512Mi"

          startupProbe:
            httpGet:
              path: /health/live
              port: 8080

            periodSeconds: 5
            failureThreshold: 30

          readinessProbe:
            httpGet:
              path: /health/ready
              port: 8080

            periodSeconds: 10
            timeoutSeconds: 2
            failureThreshold: 3

          livenessProbe:
            httpGet:
              path: /health/live
              port: 8080

            periodSeconds: 20
            timeoutSeconds: 2
            failureThreshold: 3

      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: kubernetes.io/hostname
          whenUnsatisfiable: ScheduleAnyway

          labelSelector:
            matchLabels:
              app: cloud-native-api

This is still not every production concern, but it provides a much stronger foundation than a basic Kubernetes Deployment.

Production Architecture

Users

Kubernetes Service

Deployment

Readiness + Liveness + Startup Checks

Pod 1 Pod 2 Pod 3

Resource Management + Autoscaling

Controlled Rolling Updates

Monitoring and Recovery

What I Learned From Production Kubernetes

One of the biggest lessons for me was that Kubernetes does not automatically make an application production-ready.

Kubernetes provides the platform and mechanisms, but engineers still need to define how the application should behave.

That includes decisions around:

  • Application health

  • Resource requirements

  • Scaling

  • Configuration

  • Secrets

  • Pod distribution

  • Deployment strategy

  • Graceful shutdown

  • Rollback and recovery

The more clearly these behaviours are defined, the more Kubernetes can help operate the application reliably.

Conclusion

In this article, I moved beyond a basic Kubernetes Deployment and explored patterns that make applications more suitable for production environments.

We covered:

  • Readiness probes

  • Liveness probes

  • Startup probes

  • CPU and memory requests

  • Resource limits

  • ConfigMaps

  • Secret handling

  • Rolling updates

  • Horizontal scaling

  • Pod distribution

  • Pod disruption budgets

  • Graceful shutdown

  • Immutable image versions

  • Rollback strategies

For me, this was an important step in understanding cloud-native engineering. Deploying containers is only one part of the problem.

The bigger challenge is designing applications and infrastructure so that failures, scaling events and deployments can happen without creating unnecessary disruption.

My next step: After building a stronger Kubernetes foundation, I started looking at how automation could help me reduce repetitive operational tasks. In the next article, I will explore how I use Python to build a practical infrastructure health checker for DevOps environments.