A production Kubernetes application quickly grows into dozens of YAML files: Deployments, Services, ConfigMaps, Secrets, Ingress rules, RBAC roles, and more.

Managing all of these manually is error-prone. Different environments (dev, staging, production) need different values. Sharing your app with others means sending them a bundle of raw YAML.

Helm solves this. It is the package manager for Kubernetes — think npm for Node.js or apt for Ubuntu, but for Kubernetes applications.

What is Helm?

Helm packages Kubernetes YAML files into charts — versioned, reusable packages. A chart can include everything needed to deploy an application.

Key Helm concepts:

TermWhat it is
ChartA package of Kubernetes YAML templates
ReleaseA deployed instance of a chart
RepositoryA collection of charts (like a package registry)
ValuesConfiguration that customizes a chart

A chart can be installed multiple times with different configurations. Each installation is a separate release.

Installing Helm

macOS (Homebrew):

brew install helm

Linux:

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

Windows (winget):

winget install Helm.Helm

Verify:

helm version

Installing an App with Helm

The Bitnami repository has high-quality charts for popular apps. Add it:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update

Search for available charts:

helm search repo postgresql

Install PostgreSQL:

helm install my-postgres bitnami/postgresql \
  --set auth.username=alex \
  --set auth.password=YOUR_SECURE_PASSWORD \
  --set auth.database=myappdb

Helm deploys PostgreSQL and all its Kubernetes resources in seconds.

Check the release:

helm list
NAME          NAMESPACE   REVISION   STATUS     CHART                    APP VERSION
my-postgres   default     1          deployed   postgresql-16.4.12       16.6.0

Check the running Pods:

kubectl get pods
NAME                    READY   STATUS    RESTARTS   AGE
my-postgres-0           1/1     Running   0          60s

Helm Commands Reference

# Add a repository
helm repo add bitnami https://charts.bitnami.com/bitnami

# Update repository index
helm repo update

# Search for charts
helm search repo postgresql
helm search hub prometheus   # Search Artifact Hub (all public repos)

# Install a chart
helm install <release-name> <chart> [--set key=value]

# Install from a values file
helm install my-postgres bitnami/postgresql -f my-values.yaml

# List all releases
helm list
helm list -A   # All namespaces

# Check release status
helm status my-postgres

# Upgrade a release
helm upgrade my-postgres bitnami/postgresql --set image.tag=16.6.0

# Upgrade or install (idempotent)
helm upgrade --install my-postgres bitnami/postgresql

# Roll back to previous version
helm rollback my-postgres

# Roll back to a specific revision
helm rollback my-postgres 1

# Delete a release
helm uninstall my-postgres

# Show chart values
helm show values bitnami/postgresql

# Get rendered YAML without deploying (dry run)
helm install my-postgres bitnami/postgresql --dry-run

Using a Values File

Instead of long --set flags, use a values file:

# my-values.yaml
auth:
  username: alex
  password: YOUR_SECURE_PASSWORD
  database: myappdb

primary:
  resources:
    requests:
      cpu: 250m
      memory: 256Mi
    limits:
      cpu: 500m
      memory: 512Mi

persistence:
  size: 10Gi
helm install my-postgres bitnami/postgresql -f my-values.yaml

Values files are easier to read, can be version-controlled, and can be overridden per environment.

Creating Your Own Chart

When your app has many YAML files, package them into a Helm chart.

Create a chart scaffold:

helm create myapp

This creates:

myapp/
├── Chart.yaml          # Chart metadata (name, version, description)
├── values.yaml         # Default configuration values
├── templates/          # Kubernetes YAML templates
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── serviceaccount.yaml
│   └── _helpers.tpl    # Template helper functions
└── charts/             # Dependent charts

Chart.yaml

# myapp/Chart.yaml
apiVersion: v2
name: myapp
description: My web application
type: application
version: 0.1.0          # Chart version
appVersion: "1.0.0"     # App version

values.yaml

# myapp/values.yaml
replicaCount: 2

image:
  repository: myapp
  tag: "1.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

resources:
  requests:
    cpu: 100m
    memory: 64Mi
  limits:
    cpu: 250m
    memory: 128Mi

Template Syntax

Helm uses Go template syntax. Values from values.yaml are referenced with {{ .Values.key }}:

# myapp/templates/deployment.yaml
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:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 80
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Install Your Chart

# Install from local directory
helm install myapp ./myapp

# Install with custom values
helm install myapp ./myapp --set replicaCount=3

# Install for staging with a values override
helm install myapp-staging ./myapp -f staging-values.yaml

Lint and Test Your Chart

# Check for errors
helm lint ./myapp

# See rendered YAML without deploying
helm template ./myapp

# Test after installing
helm test myapp

Environment Promotion

A common pattern: one chart, different values per environment.

myapp/
├── values.yaml              # Shared defaults
├── values-dev.yaml          # Dev overrides
├── values-staging.yaml      # Staging overrides
└── values-prod.yaml         # Production overrides
# values-prod.yaml
replicaCount: 5

resources:
  requests:
    cpu: 500m
    memory: 256Mi
  limits:
    cpu: 1000m
    memory: 512Mi
# Deploy to production
helm upgrade --install myapp ./myapp -f values.yaml -f values-prod.yaml

Artifact Hub

Artifact Hub (artifacthub.io) is the official directory for Helm charts. It lists charts from many publishers, including CNCF projects and major vendors.

Search for charts before building your own — there is likely already a well-maintained chart for popular software.

# Search from the CLI
helm search hub grafana --output yaml

Common Mistakes

Storing secrets in values.yaml

values.yaml is committed to git. Never put passwords or API keys in it. Use Kubernetes Secrets with secretKeyRef, or integrate with External Secrets Operator. Pass sensitive values via --set in CI/CD pipelines (stored as CI secrets).

# CI/CD: pass the secret at deploy time, not in the values file
helm upgrade --install myapp ./myapp \
  -f values-prod.yaml \
  --set db.password=$DB_PASSWORD

Not pinning chart versions

Running helm upgrade without pinning the chart version can pull a new chart version with breaking changes.

# Specify the chart version
helm install my-postgres bitnami/postgresql --version 16.4.12

Not using --atomic in CI

If a Helm install fails partway, you get a broken release. Use --atomic to automatically roll back on failure:

helm upgrade --install myapp ./myapp \
  -f values-prod.yaml \
  --atomic \
  --timeout 5m

What’s Next?

Your applications are packaged as Helm charts. The next step is knowing what happens when they run — setting up monitoring with Prometheus and Grafana.

Next: Kubernetes Tutorial #8: Monitoring with Prometheus and Grafana