Helm Charts for Kubernetes: A Practical DevOps Guide

August 7, 2026

If you've ever hand-written Kubernetes YAML files for a real application, you know the pain. A simple web app needs a Deployment, a Service, an Ingress, a ConfigMap, maybe a HorizontalPodAutoscaler — and every environment (dev, staging, production) needs slightly different values. Multiply that by ten microservices and you're drowning in copy-pasted YAML.

Helm solves this. It's the package manager for Kubernetes — think apt or brew but for cluster workloads. Instead of managing raw YAML, you work with charts: templated, versioned, reusable packages that install with a single command.

Why Helm Matters

Without Helm, deploying an app to Kubernetes means maintaining a growing pile of YAML files. Need to change the replica count for staging? Edit the file. Need to add an environment variable? Edit the file. Want to roll back? Hope you kept a copy of the old YAML.

With Helm:

Installing Helm

On macOS with Homebrew:

brew install helm

On Linux:

curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Verify the installation:

helm version
# version.BuildInfo{Version:"v3.16.x", ...}

Helm 3 is the current major version. If you see references to Tiller, that's Helm 2 — ignore it. Helm 3 removed the server-side component entirely and talks directly to the Kubernetes API.

Installing Your First Chart

The fastest way to get started is installing a chart from a public repository. Let's deploy NGINX:

# Add the Bitnami repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

# Install NGINX
helm install my-nginx bitnami/nginx \
  --namespace demo --create-namespace \
  --set service.type=ClusterIP

# Check the release
helm list -n demo
kubectl get pods -n demo

That single helm install command created a Deployment, a Service, and all the supporting resources. No YAML files to maintain. To customize values, you can pass them inline with --set or use a values file.

Understanding Chart Structure

Create a new chart to see how it works:

helm create my-app
tree my-app/

The generated structure:

my-app/
├── Chart.yaml          # Chart metadata (name, version, description)
├── values.yaml         # Default configuration values
├── charts/             # Dependencies (sub-charts)
├── templates/
│   ├── deployment.yaml # Kubernetes resource templates
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── hpa.yaml
│   ├── serviceaccount.yaml
│   ├── _helpers.tpl    # Template helpers (partial templates)
│   ├── NOTES.txt       # Post-install message
│   └── tests/
│       └── test-connection.yaml
└── .helmignore         # Files to exclude when packaging

The key files are Chart.yaml (who am I?), values.yaml (what are my defaults?), and everything under templates/ (what do I create in the cluster?).

Templates: Where the Magic Happens

Helm templates are Kubernetes YAML files with Go template expressions embedded. Here's a simplified Deployment template:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          env:
            {{- range .Values.env }}
            - name: {{ .name }}
              value: {{ .value | quote }}
            {{- end }}

Every {{ .Values.xxx }} expression pulls from values.yaml or user overrides. This is the core idea: the template describes the shape of your resources, and values fill in the specifics.

Customizing with Values Files

Instead of passing dozens of --set flags, create per-environment values files:

# values-staging.yaml
replicaCount: 1
image:
  repository: myapp
  tag: staging-latest
  pullPolicy: Always
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 200m
    memory: 256Mi

# values-production.yaml
replicaCount: 3
image:
  repository: myapp
  tag: v1.2.0
  pullPolicy: IfNotPresent
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi

Deploy with the file:

helm install myapp ./my-app \
  -f values-production.yaml \
  -n production --create-namespace

Helm merges values in order: values.yaml (defaults) → -f files (left to right) → --set flags (highest priority). This layering lets you keep a sane base and override only what differs per environment.

Releases and Rollbacks

Every helm install creates a release — a named deployment with a revision history stored as Kubernetes Secrets.

# Upgrade a release
helm upgrade myapp ./my-app -f values-production.yaml

# See release history
helm history myapp

# Rollback to revision 2
helm rollback myapp 2

# Uninstall completely
helm uninstall myapp -n production

Rollbacks are instant because Helm stores the full manifest of every revision. This alone makes Helm worth adopting — it gives you deployment history without needing an external tool.

Dependency Management

Charts can depend on other charts. A common pattern is depending on Redis or PostgreSQL as a sub-chart:

# Chart.yaml
dependencies:
  - name: redis
    version: "18.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
# Update dependencies
helm dependency update ./my-app

# This downloads the redis chart into charts/

The condition field lets you toggle dependencies per environment. Production might use an external Redis instance while dev uses the bundled sub-chart.

Packaging and Sharing

Package your chart for distribution:

# Lint first
helm lint ./my-app

# Package as a .tgz archive
helm package ./my-app
# Creates my-app-0.1.0.tgz

# Push to an OCI registry (GitHub Container Registry, ECR, etc.)
helm push my-app-0.1.0.tgz oci://ghcr.io/myorg/charts

OCI-based registries are now the standard for chart distribution. No need to maintain a separate chart repository server — your container registry does double duty.

Debugging Helm Charts

When things go wrong, these commands save you time:

# Render templates locally without installing (dry-run)
helm template myapp ./my-app -f values-production.yaml

# Dry-run against the cluster (validates against API server)
helm install myapp ./my-app --dry-run --debug

# See what would change in an upgrade
helm diff upgrade myapp ./my-app -f values-production.yaml
# (requires helm-diff plugin: helm plugin install https://github.com/databus23/helm-diff)

The helm template command is invaluable during development — it shows you the exact Kubernetes YAML that will be applied, without touching the cluster. Pair it with helm diff for upgrades and you'll rarely be surprised by what lands in your cluster.

Summary

Helm transforms Kubernetes deployment from ad-hoc YAML management into a structured, repeatable process:

Start by converting one of your existing YAML deployments into a Helm chart. Replace hardcoded values with template expressions, create per-environment value files, and use helm upgrade for deployments. Once you experience rollback with helm history and helm rollback, you'll never go back to managing raw YAML.