The Best Way to Deploy Kubernetes Applications in 2026

The Best Way to Deploy Kubernetes Applications in 2026

The first thing you learn when you start with Kubernetes is kubectl apply -f deployment.yaml. The second thing you learn is that this approach doesn’t scale, doesn’t provide auditability, and is a nightmare to manage across multiple environments.

By now, Kubernetes deployment has matured enough that “best practices” are actually knowable. Here’s the stack that makes sense for production deployments in 2026.

The Non-Starters

First, let’s eliminate the approaches that seem reasonable but aren’t:

Plain kubectl apply: No release management. No rollback mechanism. No audit trail of what changed when. Fine for learning, not for production.

kubectl edit: Absolutely not. Every change bypasses your CI/CD pipeline, produces no audit trail, and creates drift between your Git repo and cluster state.

helm install from a developer’s laptop: Good for local development. Terrible for production. You want your deployments coming from a CI system, not from someone’s machine with a different values file than everyone else.

Copying manifests between environments by hand: The fastest way to ensure your environments diverge in ways that are subtle and painful to debug.

The Production Stack

The approach that works:

Git Repository (source of truth)
        ↓
  Helm Chart + Values
        ↓
  CI Pipeline (lint, test, build image, push)
        ↓
  GitOps Controller (Flux/Argo CD)
        ↓
  Kubernetes Cluster

Every piece matters.

Helm: The Templating Layer

Helm is the de facto standard for packaging Kubernetes applications. Your application’s Kubernetes manifests belong in a Helm chart—even if you’re not publishing it to a public repository, even if you’re the only consumer.

Why? Because Helm gives you:

  • Parameterization: One chart, multiple values files for each environment
  • Release management: helm rollback works (though GitOps is better)
  • Dependency management: Chart.yaml dependencies for multi-component apps
  • Hooks: Pre-install, post-upgrade, pre-delete lifecycle hooks
  • Test hooks: helm test for post-deployment validation

A well-structured chart layout:

my-app/
├── Chart.yaml
├── values.yaml              # Default values
├── values-staging.yaml      # Staging overrides
├── values-production.yaml   # Production overrides
└── templates/
    ├── _helpers.tpl
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── configmap.yaml
    ├── secret.yaml
    ├── hpa.yaml
    ├── poddisruptionbudget.yaml
    └── serviceaccount.yaml

Immutable Container Images

Non-negotiable: every deployment should reference a specific, immutable image tag. Never deploy :latest. Never.

# Bad
image: myapp:latest

# Good
image: myregistry.example.com/myapp:1.4.2-abc1234

Your CI pipeline builds an image, tags it with the git commit SHA or semantic version, pushes it to your registry, and updates the reference in your Helm values or Kustomize overlay. This is what you deploy. Every deployment is reproducible and auditable.

GitOps: The Deployment Mechanism

Once you have a Helm chart and an image tag, the deployment mechanism should be GitOps. Either Flux or Argo CD—both work well, choose based on your team’s preferences (see my comparison of Argo CD vs Flux).

The key concept: a commit to your deploy repo triggers reconciliation, not a manual command.

Your workflow:

  1. Developer merges PR to application repo
  2. CI builds and pushes new image myapp:1.4.3-def5678
  3. CI (or image automation) opens a PR against the deploy repo updating the image tag
  4. PR is reviewed and merged
  5. GitOps controller detects the change and reconciles the cluster

At no point did anyone kubectl apply or helm upgrade anything. The cluster is a function of your Git history.

# flux HelmRelease referencing the values
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
  name: my-app
  namespace: my-app
spec:
  interval: 30m
  chart:
    spec:
      chart: my-app
      version: ">=1.0.0 <2.0.0"
      sourceRef:
        kind: HelmRepository
        name: my-charts
        namespace: flux-system
  values:
    image:
      tag: "1.4.3-def5678"
    replicaCount: 3
    resources:
      requests:
        memory: "128Mi"
        cpu: "100m"
      limits:
        memory: "256Mi"

Environment Promotion

Your GitOps repository should have separate paths for each environment:

clusters/
├── staging/
│   ├── my-app/
│   │   ├── helmrelease.yaml   # references values-staging.yaml
│   │   └── namespace.yaml
└── production/
    ├── my-app/
    │   ├── helmrelease.yaml   # references values-production.yaml
    │   └── namespace.yaml

Promoting from staging to production means updating the image tag in the production HelmRelease and merging. The same process, the same audit trail.

Health Checks and Rollout Strategy

Your Deployment should have meaningful health probes:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5
  failureThreshold: 3

Use a rolling update strategy with maxUnavailable: 0 to ensure zero-downtime deployments:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxUnavailable: 0
    maxSurge: 1

Configure terminationGracePeriodSeconds long enough for your application to finish in-flight requests (typically 30-60 seconds for most apps).

PodDisruptionBudgets

Often forgotten, always important:

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: my-app

Without a PDB, Kubernetes can evict all your pods simultaneously during node maintenance. With a PDB, at least one pod stays available. For production workloads with more than one replica, this should be standard.

Resource Management

Every container should have resource requests and limits:

resources:
  requests:
    cpu: "100m"
    memory: "128Mi"
  limits:
    cpu: "500m"
    memory: "512Mi"

Requests affect scheduling—the scheduler uses them to find a node with sufficient capacity. Limits affect runtime—a container exceeding its memory limit gets OOMKilled.

Rule of thumb: set limits to 2-4x your requests. Monitor actual usage with tools like Goldilocks or VPA (Vertical Pod Autoscaler) in recommendation mode, then tune.

Secrets: SOPS or External Secrets Operator

Never put plaintext secrets in your GitOps repo. Two good options:

SOPS (Mozilla): Encrypt secrets in your Git repo with age or PGP. Flux can decrypt them natively. Simple, no additional infrastructure.

External Secrets Operator: Pull secrets from Vault, AWS SSM, Google Secret Manager, Azure Key Vault, etc. More complex to set up, but better for organizations with existing secret management infrastructure.

# External Secrets example
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: my-app-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: my-app-secrets
  data:
    - secretKey: db-password
      remoteRef:
        key: my-app/production
        property: db-password

The Complete Checklist

Before declaring a deployment production-ready:

  • Helm chart with parameterized values per environment
  • Immutable image tags (no :latest)
  • GitOps controller managing reconciliation
  • Liveness and readiness probes configured
  • Rolling update strategy with maxUnavailable: 0
  • PodDisruptionBudget defined
  • Resource requests and limits set
  • Secrets encrypted or externalized
  • HPA configured for variable-load services
  • terminationGracePeriodSeconds tuned
  • Network policies defined
  • ServiceAccount with least-privilege RBAC

Do all of this and you’ve got a deployment that’s repeatable, auditable, resilient, and will handle most production failures gracefully. Skip items from this list and you’ll encounter each omission at the worst possible time.

Share this post: LinkedIn Reddit WhatsApp Mastodon
Jesse Borden

Jesse Borden

Software Engineer with an interest in hands on learning

I have several years of professional Information Technology (IT) experience leading staff and projects within the Department of War (DOW). I have managed Service Desk, Web Application Development, and System Administration teams. My two greatest passions are learning and conti...