Back to Distributed Systems & Kubernetes Series

Helm Track Part 1: Install & First Deploy

June 6, 2026 Wasil Zafar 22 min read

Raw Kubernetes YAML quickly becomes unmanageable across environments. Helm is the package manager that solves this — parameterised charts, versioned releases, and one-command rollbacks. Let's install it and deploy real workloads, including the grade-api we'll use throughout this track.

Table of Contents

  1. Why Helm Exists
  2. Installing Helm
  3. Chart Repositories
  4. First Deploy: nginx-ingress
  5. Deploying grade-api
  6. Upgrade & Rollback
  7. Release Lifecycle Commands
  8. Exercises
  9. Key Takeaways & Next Steps

Why Helm Exists

The Raw YAML Problem

Imagine deploying the grade-api — a simple REST API with a PostgreSQL database — to three environments: development, staging, and production. Each environment needs different replica counts, different image tags, different resource limits, different database hostnames, and different secret references. With raw YAML you have three options, all bad:

  • Copy YAML three times: Drift is inevitable. A critical security fix in prod never makes it to staging.
  • Use sed/envsubst: Fragile, hard to test, impossible to track what's deployed where.
  • One YAML with all environments: Impossible — the differences are too deep.
The Analogy: Helm is to Kubernetes what apt/brew/pip is to software packages. A chart is a package definition. A release is an installed instance of that package. You can have multiple releases of the same chart (grade-api-dev, grade-api-prod) each with different values.

Charts, Releases, Repositories

Three concepts underpin everything in Helm:

  • Chart: A bundle of YAML templates, a values.yaml with defaults, and metadata. Stored as a tarball. Versioned with semver.
  • Release: A named, running instance of a chart in a cluster. Helm tracks release state in Kubernetes Secrets (in the same namespace). Running helm list shows all releases.
  • Repository: An HTTP server (or OCI registry) that indexes and serves chart tarballs. ArtifactHub.io is the public discovery layer.
Helm Concepts: Chart → Release → Cluster
flowchart LR
    REPO[Chart Repository
bitnami, ingress-nginx] CHART[Chart
nginx-ingress v4.x.x] VALUES[values.yaml
overrides] RELEASE1[Release: ingress-prod
namespace: ingress-nginx] RELEASE2[Release: ingress-dev
namespace: dev] K8S[Kubernetes Objects
Deployment, Service, CRDs...] REPO --> CHART CHART --> RELEASE1 CHART --> RELEASE2 VALUES --> RELEASE1 VALUES --> RELEASE2 RELEASE1 --> K8S RELEASE2 --> K8S style REPO fill:#e8f4f4,stroke:#3B9797,color:#132440 style CHART fill:#e8f4f4,stroke:#3B9797,color:#132440 style RELEASE1 fill:#f0f4f8,stroke:#16476A,color:#132440 style RELEASE2 fill:#f0f4f8,stroke:#16476A,color:#132440 style K8S fill:#fff5f5,stroke:#BF092F,color:#132440

Installing Helm

Prerequisite: You need a running Kubernetes cluster and kubectl configured. If you haven't set one up yet, refer to the Minikube or Kind tracks in this series. Helm reads your ~/.kube/config — no extra configuration needed.

macOS

# Install via Homebrew (recommended)
brew install helm

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

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

Windows

# Install via Chocolatey
choco install kubernetes-helm

# Install via Scoop
scoop install helm

# Install via winget
winget install Helm.Helm

# Verify
helm version

Linux

# Method 1: Script (detects distro automatically)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Method 2: apt (Debian/Ubuntu) — via official signing key
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | \
  sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] \
  https://baltocdn.com/helm/stable/debian/ all main" | \
  sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

# Method 3: Binary release
HELM_VERSION="v3.15.3"
curl -fsSL "https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz" | tar xz
sudo mv linux-amd64/helm /usr/local/bin/helm

helm version

Chart Repositories

Adding Repositories

A repository is just an index.yaml file served over HTTP. Helm caches this locally. The most commonly used repos:

# Add popular repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add cert-manager https://charts.jetstack.io
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo add argo https://argoproj.github.io/argo-helm

# Update local cache (like apt-get update)
helm repo update

# List configured repos
helm repo list
# NAME                    URL
# bitnami                 https://charts.bitnami.com/bitnami
# ingress-nginx           https://kubernetes.github.io/ingress-nginx
# ...
# Search ArtifactHub (public index — requires internet)
helm search hub postgres

# Search local repo cache
helm search repo postgres
# NAME                            CHART VERSION   APP VERSION
# bitnami/postgresql              16.x.x          16.x.x
# bitnami/postgresql-ha           ...             ...

# Show all available versions of a chart
helm search repo bitnami/postgresql --versions | head -10

# Inspect chart before installing — see all configurable values
helm show values bitnami/postgresql | head -60

# Inspect chart README
helm show readme bitnami/postgresql | head -30
Real World

ArtifactHub vs Private Registries

Public repos like Bitnami are fine for open-source components (PostgreSQL, Redis, nginx). For your own services, most teams push charts to an OCI registry — AWS ECR, GCP Artifact Registry, Azure ACR, or Harbor. OCI support in Helm 3.8+ means you use oci://registry/repo/chart:tag as the chart reference. We cover OCI publishing in Part 4.

OCI Harbor Artifact Registry

First Deploy: nginx Ingress Controller

helm install

Let's deploy the nginx Ingress Controller — a real production-grade workload — to understand how helm install works:

# Create a dedicated namespace
kubectl create namespace ingress-nginx

# Install the chart
# Format: helm install   [flags]
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --set controller.replicaCount=2 \
  --set controller.resources.requests.cpu=100m \
  --set controller.resources.requests.memory=128Mi

# What just happened?
# 1. Helm fetched the chart from the ingress-nginx repo
# 2. Rendered templates with your --set overrides
# 3. Applied all resulting manifests to the cluster
# 4. Stored release metadata as a Secret in the namespace
# 5. Waited for resources to become ready (default: no wait)
# Add --wait to block until all resources are ready
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --wait \
  --timeout 5m

# NAME: ingress-nginx
# LAST DEPLOYED: Fri Jun  6 10:00:00 2026
# NAMESPACE: ingress-nginx
# STATUS: deployed
# REVISION: 1

Inspecting a Release

# List all releases across all namespaces
helm list -A

# List releases in a specific namespace
helm list -n ingress-nginx

# NAME            NAMESPACE       REVISION  STATUS    CHART                         APP VERSION
# ingress-nginx   ingress-nginx   1         deployed  ingress-nginx-4.x.x           1.11.x

# Show the computed manifests Helm applied (what's actually running)
helm get manifest ingress-nginx -n ingress-nginx

# Show the values used for this release (includes defaults + your overrides)
helm get values ingress-nginx -n ingress-nginx

# Show all values including defaults
helm get values ingress-nginx -n ingress-nginx --all

# Show release status and notes
helm status ingress-nginx -n ingress-nginx

Deploying grade-api with Helm

Throughout this track we use grade-api as our running example — a REST API that stores student grades in PostgreSQL. In Part 2 we'll write its Helm chart from scratch. For now, let's deploy it using raw values passed to helm install with Bitnami's generic web app pattern, then compare that to the chart-native approach.

The Manual YAML Baseline

This is what we want to stop writing by hand for every environment:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: grade-api
  namespace: default
spec:
  replicas: 2
  selector:
    matchLabels:
      app: grade-api
  template:
    metadata:
      labels:
        app: grade-api
        version: v1.0.0
    spec:
      containers:
      - name: grade-api
        image: ghcr.io/example/grade-api:v1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: DB_HOST
          valueFrom:
            configMapKeyRef:
              name: grade-api-config
              key: db.host
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: grade-api-secrets
              key: db-password
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 256Mi
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: grade-api
  namespace: default
spec:
  selector:
    app: grade-api
  ports:
  - port: 80
    targetPort: 8080

That's just two resources. Production grade-api also needs a ConfigMap, a Secret, an HPA, a PodDisruptionBudget, and an Ingress. That's 600+ lines of YAML that varies between environments. Helm makes this a values.yaml change.

Deploying PostgreSQL for grade-api

Let's deploy PostgreSQL using Bitnami's chart — this is exactly how a real project starts:

# Create a values file for grade-api's database
cat > postgres-values.yaml <<'EOF'
auth:
  database: grades
  username: grade_user
  password: "changeme-use-secrets-in-prod"

primary:
  resources:
    requests:
      cpu: 250m
      memory: 256Mi
    limits:
      cpu: 1
      memory: 512Mi
  persistence:
    size: 10Gi

metrics:
  enabled: true
EOF

# Deploy PostgreSQL
helm install grade-db bitnami/postgresql \
  --namespace grade-api \
  --create-namespace \
  --values postgres-values.yaml \
  --wait

# Verify
kubectl get pods -n grade-api
# NAME                        READY   STATUS    RESTARTS   AGE
# grade-db-postgresql-0       2/2     Running   0          45s

# Connect to verify (optional)
kubectl run pg-client --rm -it --restart=Never \
  --image=bitnami/postgresql:16 \
  --namespace grade-api \
  -- psql -h grade-db-postgresql -U grade_user -d grades

Upgrade & Rollback

helm upgrade --install

helm upgrade --install is the idempotent pattern you'll use in CI/CD pipelines — it installs if the release doesn't exist, upgrades if it does. This is the command for automated deployments.

# Upgrade to a new chart version
helm upgrade grade-db bitnami/postgresql \
  --namespace grade-api \
  --values postgres-values.yaml \
  --set primary.resources.limits.memory=1Gi \
  --wait

# Idempotent install-or-upgrade (CI/CD pattern)
helm upgrade --install grade-db bitnami/postgresql \
  --namespace grade-api \
  --create-namespace \
  --values postgres-values.yaml \
  --wait \
  --atomic    # Roll back automatically if upgrade fails

# Pin to a specific chart version (important for reproducibility!)
helm upgrade --install grade-db bitnami/postgresql \
  --namespace grade-api \
  --version 16.2.3 \
  --values postgres-values.yaml

# Dry run — see what would change without applying
helm upgrade grade-db bitnami/postgresql \
  --namespace grade-api \
  --values postgres-values.yaml \
  --dry-run
Always pin chart versions in production. helm upgrade ... bitnami/postgresql without --version pulls the latest chart version, which may introduce breaking changes. Use --version X.Y.Z and update deliberately. Track chart versions in your Git repository alongside your values.yaml.

helm rollback

# View release history (Helm stores 10 revisions by default)
helm history grade-db -n grade-api

# REVISION  STATUS      CHART                   DESCRIPTION
# 1         superseded  postgresql-16.2.0       Install complete
# 2         superseded  postgresql-16.2.3       Upgrade complete
# 3         deployed    postgresql-16.2.3       Upgrade complete

# Roll back to a specific revision
helm rollback grade-db 1 -n grade-api

# Roll back to previous revision (shorthand)
helm rollback grade-db -n grade-api

# Rollback with wait
helm rollback grade-db 1 -n grade-api --wait

# After rollback, history shows the new revision
helm history grade-db -n grade-api
# REVISION  STATUS      CHART                   DESCRIPTION
# 1         superseded  postgresql-16.2.0       Install complete
# 2         superseded  postgresql-16.2.3       Upgrade complete
# 3         superseded  postgresql-16.2.3       Upgrade complete
# 4         deployed    postgresql-16.2.0       Rollback to 1
Case Study

Rollback Doesn't Roll Back Data

A common misconception: helm rollback rolls back Kubernetes object definitions (Deployments, ConfigMaps, Services) but does not roll back PersistentVolume data. If a database migration ran during an upgrade and you roll back the chart, the schema migration is still there. Always design schema changes to be backward-compatible and plan database rollbacks separately from Helm rollbacks.

StatefulSets Database Migrations Backward Compatibility

Release History & Secrets

Helm stores each release revision as a Kubernetes Secret in the release's namespace. This means release history survives pod restarts — it's stored in etcd, not Helm's local state:

# Release history is stored as Secrets
kubectl get secrets -n grade-api -l owner=helm

# NAME                        TYPE                    DATA   AGE
# sh.helm.release.v1.grade-db.v1   helm.sh/release.v1   1      5m
# sh.helm.release.v1.grade-db.v2   helm.sh/release.v1   1      3m
# sh.helm.release.v1.grade-db.v3   helm.sh/release.v1   1      1m

# Control history size (saves space in etcd)
helm upgrade grade-db bitnami/postgresql \
  --namespace grade-api \
  --history-max 5 \
  --values postgres-values.yaml

Release Lifecycle Commands

A quick reference for the complete Helm release lifecycle:

# ── DISCOVERY ──────────────────────────────────────────────
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/postgresql --versions
helm show values bitnami/postgresql

# ── INSTALL ────────────────────────────────────────────────
helm install my-release chart/name \
  --namespace my-ns \
  --create-namespace \
  --values values.yaml \
  --set key=value \
  --version 1.2.3 \
  --wait --timeout 5m

# ── INSPECT ────────────────────────────────────────────────
helm list -n my-ns
helm status my-release -n my-ns
helm get values my-release -n my-ns
helm get manifest my-release -n my-ns
helm history my-release -n my-ns

# ── UPGRADE ────────────────────────────────────────────────
helm upgrade --install my-release chart/name \
  --namespace my-ns \
  --values values.yaml \
  --atomic --wait

# ── ROLLBACK ───────────────────────────────────────────────
helm rollback my-release [REVISION] -n my-ns --wait

# ── UNINSTALL ──────────────────────────────────────────────
helm uninstall my-release -n my-ns
# Keeps history by default; use --keep-history to retain Secrets

Exercises

Exercise 1 — Deploy Redis: Add the Bitnami repo and deploy Redis in a cache namespace using helm install. Override the replica count to 1. Verify with kubectl get pods -n cache. Then upgrade to enable persistence (--set master.persistence.enabled=true) and observe helm history.
Exercise 2 — Break and Roll Back: Deploy the Bitnami nginx chart. Then upgrade with an intentionally bad value (--set replicaCount=not-a-number). Observe the failure. Roll back to revision 1. Confirm the rollback with helm status and kubectl get pods.
Exercise 3 — CI/CD Pattern: Write a bash script that uses helm upgrade --install --atomic --wait to deploy PostgreSQL. Run it twice — the first run installs, the second run is a no-op (same values). Verify that helm history shows revision 2 as "Upgrade complete" with no actual changes. This is the idempotent pattern all CI pipelines should use.
Exercise 4 — Dry Run & Diff: Install the helm diff plugin (helm plugin install https://github.com/databus23/helm-diff). Run helm diff upgrade on an existing release with a minor values change. Understand the output before applying. This is essential for safe production upgrades.

Key Takeaways & Next Steps

Key Takeaways:
  • Helm solves the multi-environment YAML duplication problem through parameterised charts and named releases
  • helm upgrade --install --atomic --wait is the correct idempotent CI/CD pattern
  • Always pin chart versions with --version in production — never leave it floating
  • Release history lives in Kubernetes Secrets — it survives pod restarts and cluster redeployments
  • helm rollback rolls back object definitions, not PersistentVolume data
  • Use helm diff plugin before any production upgrade

Next in This Track

In Part 2: Chart Structure & Templates, we write the grade-api Helm chart from scratch — directory layout, Chart.yaml, values.yaml, Go template syntax, and the built-in objects that make Helm charts powerful.