VOOZH about

URL: https://tech-insider.org/kubernetes-helm-chart-tutorial-deploy-applications-2026/

⇱ Helm Charts: Deploy K8s Apps in 5 Steps [2026]


Skip to content
April 2, 2026
28 min read

Helm has become the de facto package manager for Kubernetes, and with the release of Helm 4 at KubeCon 2025, deploying applications to Kubernetes clusters has never been more powerful. This complete helm chart tutorial walks you through every step – from installing Helm 4.1.3 on your local machine to building, packaging, and deploying production-ready Helm charts on a live Kubernetes cluster. Whether you are managing a single microservice or orchestrating dozens of deployments across multiple environments, this guide gives you the tools and knowledge to ship with confidence in 2026.

By the end of this kubernetes helm tutorial, you will have a fully working Helm chart that deploys a Node.js application with an Ingress controller, ConfigMaps, Secrets, health checks, and horizontal pod autoscaling. You will also know how to publish your chart to an OCI registry, manage upgrades and rollbacks, and troubleshoot the most common Helm issues that trip up developers every day.

Prerequisites: What You Need Before Starting This Helm Chart Tutorial

Before diving into Helm, you need a working Kubernetes environment and a handful of CLI tools installed on your machine. This section covers every prerequisite with exact version numbers so you can follow along without compatibility issues. If you are brand new to containers, start with our complete Docker beginner tutorial before continuing here.

The following table lists every tool you need, the minimum version required, and how to verify your installation. All commands in this tutorial were tested on macOS 14, Ubuntu 24.04, and Windows 11 with WSL2.

ToolMinimum VersionRecommended VersionVerify CommandPurpose
Kubernetes (kubectl)1.281.31kubectl version --clientCluster management CLI
Helm4.0.04.1.3helm versionPackage manager for K8s
Docker Desktop or Minikube4.30 / 1.334.38 / 1.35docker version or minikube versionLocal Kubernetes cluster
Node.js (for sample app)20.x LTS22.x LTSnode --versionSample application runtime
Git2.402.47git --versionVersion control
A text editor (VS Code recommended)Any1.96+N/AEditing chart templates

You also need a running Kubernetes cluster. For local development, Docker Desktop’s built-in Kubernetes or Minikube works perfectly. For cloud environments, any managed Kubernetes service – Amazon EKS, Google GKE, or Azure AKS – will work. Make sure your kubectl context points to the correct cluster by running kubectl config current-context before proceeding. If you are evaluating cloud providers, our AWS vs Azure vs Google Cloud comparison breaks down Kubernetes pricing and features across all three platforms.

Finally, ensure you have cluster-admin permissions or at least the ability to create namespaces, deployments, services, and ingress resources. On managed clusters, your cloud IAM role must include Kubernetes RBAC bindings. On local clusters like Minikube or Docker Desktop, you already have full admin access by default.

Step 1: Install Helm 4 on Your Machine

Helm 4.0.0 was released on November 12, 2025, at KubeCon + CloudNativeCon North America, marking the project’s 10th anniversary and the first major version bump in six years. The latest stable release as of March 2026 is Helm 4.1.3, which includes critical bug fixes and performance improvements over the initial 4.0 release. Installing Helm is straightforward on every major operating system.

On macOS, the fastest path is Homebrew. On Linux, you can use the official install script or snap. On Windows, use Chocolatey or Scoop. The commands below install the latest stable Helm 4 release.

# macOS (Homebrew)
brew install helm

# Linux (official install script)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Linux (Snap)
sudo snap install helm --classic

# Windows (Chocolatey)
choco install kubernetes-helm

# Verify installation
helm version
# Output: version.BuildInfo{Version:"v4.1.3", GitCommit:"...", GoVersion:"go1.23.4"}

If you are upgrading from Helm 3, the migration is smooth. Helm 4 reads Helm 3 release data and chart formats without modification. Your existing values.yaml files, templates, and repository configurations continue to work. However, Helm 4 drops support for Kubernetes 1.15 and earlier, so make sure your cluster meets the minimum version requirements listed in the prerequisites table above.

After installation, add the official Bitnami and Ingress-NGINX chart repositories. These are the two repositories you will use most frequently in this tutorial and in production workloads.

# Add commonly used Helm repositories
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

# Update repository index
helm repo update

# Verify repos are added
helm repo list
# NAME URL
# bitnami https://charts.bitnami.com/bitnami
# ingress-nginx https://kubernetes.github.io/ingress-nginx

Common Pitfall #1: Running helm install immediately after adding a repo without running helm repo update first. Helm caches the repository index locally, and a stale cache means you might install an outdated chart version. Always run helm repo update before installing or upgrading charts.

Step 2: Understand Helm Architecture and Key Concepts

Before writing your first chart, you need to understand how Helm works under the hood. Helm’s architecture is deceptively simple: it takes a chart (a package of templated Kubernetes manifests), merges it with user-supplied values, renders the final YAML, and applies it to the cluster. But the details matter, especially when debugging failed deployments or managing complex release histories.

A Chart is a collection of files organized in a specific directory structure. It contains templates (Go template files that produce Kubernetes manifests), a values.yaml file (default configuration), a Chart.yaml file (metadata), and optionally helper templates, CRDs, and tests. Charts can depend on other charts – for example, your application chart might depend on the Bitnami PostgreSQL chart for its database.

A Release is an instance of a chart deployed to a cluster. When you run helm install my-app ./my-chart, Helm creates a release named β€œmy-app.” Each release tracks its revision history, so you can roll back to any previous state. Helm 4 stores release metadata as Kubernetes Secrets in the release namespace by default, which means release data is subject to Kubernetes RBAC and namespace isolation.

A Repository is a server that hosts packaged charts. The most popular public repository is Artifact Hub, which indexes over 15,000 Helm charts from hundreds of publishers as of early 2026. Helm 4 also supports OCI-compliant registries – Docker Hub, GitHub Container Registry, Amazon ECR, and Azure Container Registry can all host Helm charts natively.

Helm 4 introduces several architectural improvements over Helm 3. The most significant is server-side apply, which delegates resource creation and patching to the Kubernetes API server instead of performing client-side three-way merges. This eliminates an entire class of merge conflicts that plagued Helm 3 users. Other key changes include a redesigned WebAssembly-based plugin system, kstatus for more accurate resource readiness tracking, local content-based caching, and slog-based structured logging.

Common Pitfall #2: Confusing chart version and app version. The version field in Chart.yaml is the chart’s own version (its packaging version). The appVersion field is the version of the application being deployed. These are independent – you can bump the chart version to fix a template bug without changing the app version.

Step 3: Create Your First Helm Chart from Scratch

Now it is time to build a Helm chart from the ground up. Helm provides a scaffolding command that generates a chart skeleton with sensible defaults. You will start with this scaffold and then customize every file to understand what each piece does. This hands-on approach is the fastest way to internalize the helm chart structure.

# Create a new chart called "webapp"
helm create webapp

# Inspect the generated directory structure
tree webapp/
# webapp/
# β”œβ”€β”€ Chart.yaml
# β”œβ”€β”€ charts/
# β”œβ”€β”€ templates/
# β”‚ β”œβ”€β”€ NOTES.txt
# β”‚ β”œβ”€β”€ _helpers.tpl
# β”‚ β”œβ”€β”€ deployment.yaml
# β”‚ β”œβ”€β”€ hpa.yaml
# β”‚ β”œβ”€β”€ ingress.yaml
# β”‚ β”œβ”€β”€ service.yaml
# β”‚ β”œβ”€β”€ serviceaccount.yaml
# β”‚ └── tests/
# β”‚ └── test-connection.yaml
# └── values.yaml

The helm create command generates a fully functional chart that deploys an NGINX container. Each file serves a distinct purpose. Chart.yaml holds metadata like the chart name, version, and description. The templates/ directory contains Go templates that Helm renders into Kubernetes manifests. The values.yaml file defines default configuration values that users can override at install time. The _helpers.tpl file contains reusable template snippets (named templates) shared across all template files.

Open Chart.yaml and update it to reflect your application. This file is the identity of your chart – every other tool in the Helm ecosystem reads it to understand what your chart does.

# webapp/Chart.yaml
apiVersion: v2
name: webapp
description: A Helm chart for deploying a Node.js web application
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
 - nodejs
 - webapp
 - api
maintainers:
 - name: Your Name
 email: [email protected]

The apiVersion: v2 field tells Helm this is a Helm 3+ chart (also compatible with Helm 4). The type: application distinguishes it from a library chart, which provides only reusable templates without deploying resources directly. For a deeper understanding of how Kubernetes fits into the broader container ecosystem, see our Docker vs Kubernetes comparison.

Common Pitfall #3: Forgetting to increment the chart version field before repackaging. Helm uses semantic versioning, and chart repositories will reject a package with a version that already exists. Always bump the version in Chart.yaml before running helm package.

Step 4: Configure Values and Template Variables

The values.yaml file is the control center of your Helm chart. Every configurable parameter – image name, replica count, resource limits, feature flags – lives here. Users override these values at install time using --set flags or custom values files, which is what makes Helm charts reusable across environments. A well-designed values.yaml is the difference between a chart that works in one environment and a chart that scales across dev, staging, and production.

Replace the generated values.yaml with a configuration tuned for your Node.js application. The file below includes every parameter your chart will use, with comments explaining each one.

# webapp/values.yaml
replicaCount: 2

image:
 repository: node
 pullPolicy: IfNotPresent
 tag: "22-alpine"

nameOverride: ""
fullnameOverride: ""

serviceAccount:
 create: true
 annotations: {}
 name: ""

service:
 type: ClusterIP
 port: 80
 targetPort: 3000

ingress:
 enabled: true
 className: "nginx"
 annotations:
 nginx.ingress.kubernetes.io/rewrite-target: /
 hosts:
 - host: webapp.local
 paths:
 - path: /
 pathType: Prefix
 tls: []

resources:
 limits:
 cpu: 500m
 memory: 256Mi
 requests:
 cpu: 100m
 memory: 128Mi

autoscaling:
 enabled: true
 minReplicas: 2
 maxReplicas: 10
 targetCPUUtilizationPercentage: 70

env:
 NODE_ENV: production
 PORT: "3000"

configMap:
 APP_NAME: "My Web App"
 LOG_LEVEL: "info"

secrets:
 DATABASE_URL: "postgresql://user:pass@db:5432/myapp"
 API_KEY: "change-me-in-production"

This values file establishes a clear contract between chart author and chart user. The image block controls which container image is deployed. The service block configures the Kubernetes Service resource. The ingress block enables external traffic routing. The resources block sets CPU and memory limits – crucial for production stability. The autoscaling block enables horizontal pod autoscaling, and the env, configMap, and secrets blocks manage application configuration.

You can override any value at install time without modifying the chart. For example, to deploy with 5 replicas and a different image tag, you would run helm install my-app ./webapp --set replicaCount=5 --set image.tag=22-slim. For more complex overrides, create an environment-specific values file (like values-production.yaml) and pass it with -f values-production.yaml.

Common Pitfall #4: Putting actual secrets in values.yaml and committing them to Git. The secrets block above is for demonstration only. In production, use --set flags with CI/CD variables, sealed-secrets, or an external secrets operator like AWS Secrets Manager or HashiCorp Vault to inject sensitive values at deploy time. Never store plaintext credentials in your chart repository.

Step 5: Build Deployment and Service Templates

Templates are the heart of a Helm chart. They are standard Kubernetes YAML files enhanced with Go template syntax – double curly braces ({{ }}) that inject values, perform logic, and call helper functions. When Helm renders a chart, it processes every file in the templates/ directory, substitutes template expressions with actual values, and produces plain Kubernetes manifests that kubectl can apply.

Start with the Deployment template. Replace the generated templates/deployment.yaml with the following, which includes ConfigMap and Secret references, health checks, and proper labeling.

# webapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
 name: {{ include "webapp.fullname" . }}
 labels:
 {{- include "webapp.labels" . | nindent 4 }}
spec:
 {{- if not .Values.autoscaling.enabled }}
 replicas: {{ .Values.replicaCount }}
 {{- end }}
 selector:
 matchLabels:
 {{- include "webapp.selectorLabels" . | nindent 6 }}
 template:
 metadata:
 annotations:
 checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
 checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
 labels:
 {{- include "webapp.labels" . | nindent 8 }}
 spec:
 serviceAccountName: {{ include "webapp.serviceAccountName" . }}
 containers:
 - name: {{ .Chart.Name }}
 image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
 imagePullPolicy: {{ .Values.image.pullPolicy }}
 ports:
 - name: http
 containerPort: {{ .Values.service.targetPort }}
 protocol: TCP
 envFrom:
 - configMapRef:
 name: {{ include "webapp.fullname" . }}-config
 - secretRef:
 name: {{ include "webapp.fullname" . }}-secret
 {{- range $key, $value := .Values.env }}
 env:
 - name: {{ $key }}
 value: {{ $value | quote }}
 {{- end }}
 livenessProbe:
 httpGet:
 path: /healthz
 port: http
 initialDelaySeconds: 15
 periodSeconds: 20
 readinessProbe:
 httpGet:
 path: /ready
 port: http
 initialDelaySeconds: 5
 periodSeconds: 10
 resources:
 {{- toYaml .Values.resources | nindent 12 }}

Several techniques in this template deserve attention. The checksum/config and checksum/secret annotations force a rolling restart when ConfigMap or Secret values change – without these, Kubernetes would not detect the change because the Deployment spec itself has not changed. The envFrom block mounts all ConfigMap and Secret keys as environment variables. The livenessProbe and readinessProbe ensure Kubernetes restarts unhealthy pods and only routes traffic to ready ones.

Next, create the ConfigMap and Secret templates. These are new files that the default scaffold does not include.

# webapp/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
 name: {{ include "webapp.fullname" . }}-config
 labels:
 {{- include "webapp.labels" . | nindent 4 }}
data:
 {{- range $key, $value := .Values.configMap }}
 {{ $key }}: {{ $value | quote }}
 {{- end }}

---
# webapp/templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
 name: {{ include "webapp.fullname" . }}-secret
 labels:
 {{- include "webapp.labels" . | nindent 4 }}
type: Opaque
data:
 {{- range $key, $value := .Values.secrets }}
 {{ $key }}: {{ $value | b64enc | quote }}
 {{- end }}

The range function iterates over the map defined in values.yaml, generating a key-value pair for each entry. Secrets are Base64-encoded using the b64enc function, as required by the Kubernetes Secret spec. This pattern keeps your templates generic – you can add new configuration keys just by editing values.yaml without touching the templates.

Step 6: Deploy Your Chart to a Local Kubernetes Cluster

With your chart templates in place, it is time to deploy. Before running the actual install, always validate your chart with helm lint and preview the rendered manifests with helm template. These two commands catch 90% of issues before they ever hit the cluster.

# Lint the chart for errors
helm lint ./webapp
# ==> Linting ./webapp
# [INFO] Chart.yaml: icon is recommended
# 1 chart(s) linted, 0 chart(s) failed

# Render templates locally (dry run)
helm template my-webapp ./webapp --debug > /tmp/rendered.yaml
cat /tmp/rendered.yaml | head -50

# Install the chart to your cluster
helm install my-webapp ./webapp --namespace demo --create-namespace

# Expected output:
# NAME: my-webapp
# LAST DEPLOYED: Mon Mar 02 10:30:00 2026
# NAMESPACE: demo
# STATUS: deployed
# REVISION: 1

# Verify the deployment
kubectl get all -n demo
# NAME READY STATUS RESTARTS AGE
# pod/my-webapp-6d4f5b7c8-abc12 1/1 Running 0 30s
# pod/my-webapp-6d4f5b7c8-def34 1/1 Running 0 30s
#
# NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
# service/my-webapp ClusterIP 10.96.45.123 <none> 80/TCP 30s
#
# NAME READY UP-TO-DATE AVAILABLE AGE
# deployment.apps/my-webapp 2/2 2 2 30s

The --namespace demo --create-namespace flags deploy the chart into a dedicated namespace, which is a best practice for isolating workloads. The helm template command renders the chart locally without contacting the cluster, making it perfect for CI/CD validation. If you are building a CI/CD pipeline to automate Helm deployments, our GitHub Actions CI/CD tutorial shows you how to integrate Helm install and upgrade commands into an automated workflow.

To test the deployment locally, use kubectl port-forward to expose the service on your machine.

# Port-forward the service to localhost
kubectl port-forward svc/my-webapp -n demo 8080:80

# In another terminal, test the endpoint
curl http://localhost:8080/healthz
# {"status": "ok"}

# Check Helm release status
helm status my-webapp -n demo
# NAME: my-webapp
# LAST DEPLOYED: Mon Mar 02 10:30:00 2026
# NAMESPACE: demo
# STATUS: deployed
# REVISION: 1

# List all releases
helm list -n demo
# NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
# my-webapp demo 1 2026-03-02 10:30:00 UTC deployed webapp-0.1.0 1.0.0

Common Pitfall #5: Deploying without specifying resource requests and limits. On shared clusters, pods without resource limits can consume unbounded CPU and memory, starving other workloads. Kubernetes also uses resource requests for scheduling decisions – without them, the scheduler cannot make intelligent placement choices. Always define both requests and limits in your values file.

Step 7: Manage Upgrades, Rollbacks, and Release History

One of Helm’s most powerful features is its built-in release management. Every time you upgrade a release, Helm creates a new revision with a complete snapshot of the rendered manifests and values. This means you can roll back to any previous revision instantly – no Git archaeology required. Understanding how upgrades and rollbacks work is essential for production operations.

To upgrade a release, modify your values and run helm upgrade. Helm computes the diff between the current and desired state and applies only the changes. With Helm 4’s server-side apply, this process is more reliable than ever because the Kubernetes API server handles merge conflicts instead of the Helm client.

# Upgrade with new values (increase replicas and change log level)
helm upgrade my-webapp ./webapp -n demo 
 --set replicaCount=4 
 --set configMap.LOG_LEVEL=debug

# Output:
# Release "my-webapp" has been upgraded. Happy Helming!
# NAME: my-webapp
# LAST DEPLOYED: Mon Mar 02 11:00:00 2026
# NAMESPACE: demo
# STATUS: deployed
# REVISION: 2

# View release history
helm history my-webapp -n demo
# REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
# 1 Mon Mar 02 10:30:00 2026 superseded webapp-0.1.0 1.0.0 Install complete
# 2 Mon Mar 02 11:00:00 2026 deployed webapp-0.1.0 1.0.0 Upgrade complete

# Roll back to revision 1
helm rollback my-webapp 1 -n demo
# Rollback was a success! Happy Helming!

# Verify rollback
helm history my-webapp -n demo
# REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
# 1 Mon Mar 02 10:30:00 2026 superseded webapp-0.1.0 1.0.0 Install complete
# 2 Mon Mar 02 11:00:00 2026 superseded webapp-0.1.0 1.0.0 Upgrade complete
# 3 Mon Mar 02 11:05:00 2026 deployed webapp-0.1.0 1.0.0 Rollback to 1

Notice that rollback creates a new revision (3) rather than deleting revision 2. This preserves the complete audit trail. By default, Helm keeps the last 10 revisions. You can change this with --history-max on install or upgrade. In production, set this to at least 5 to maintain enough rollback targets without accumulating excessive Secrets in the namespace.

For zero-downtime upgrades, combine Helm with Kubernetes rolling update strategy. The default deployment strategy in Kubernetes is RollingUpdate with maxUnavailable: 25% and maxSurge: 25%. This means Kubernetes gradually replaces old pods with new ones, ensuring at least 75% of your desired replicas are always available during the upgrade.

Step 8: Use Helm Dependencies and Subcharts

Real applications rarely run in isolation. Your web app needs a database, a cache layer, maybe a message queue. Helm handles this with chart dependencies – you declare the charts your application depends on, and Helm downloads and installs them alongside your chart. This is the Helm equivalent of package.json dependencies in Node.js or requirements.txt in Python.

Add a PostgreSQL dependency to your chart by editing Chart.yaml.

# Add to webapp/Chart.yaml
dependencies:
 - name: postgresql
 version: "16.4.1"
 repository: "https://charts.bitnami.com/bitnami"
 condition: postgresql.enabled

# Then run dependency update
helm dependency update ./webapp

# Output:
# Saving 1 charts
# Downloading postgresql from repo https://charts.bitnami.com/bitnami
# Deleting outdated charts

# This creates webapp/charts/postgresql-16.4.1.tgz

Now configure the PostgreSQL subchart through your parent chart’s values.yaml. Subchart values are namespaced under the dependency name.

# Add to webapp/values.yaml
postgresql:
 enabled: true
 auth:
 username: webapp_user
 password: "" # Set via --set at deploy time
 database: webapp_db
 primary:
 persistence:
 size: 10Gi
 resources:
 limits:
 cpu: 500m
 memory: 512Mi
 requests:
 cpu: 250m
 memory: 256Mi

The condition: postgresql.enabled field in Chart.yaml makes the dependency optional. Setting postgresql.enabled=false at install time skips the PostgreSQL deployment entirely, which is useful when your staging environment uses an external database service. This pattern – conditional dependencies controlled by values – is a Helm best practice that keeps charts flexible across environments.

Common Pitfall #6: Forgetting to run helm dependency update after modifying the dependencies section of Chart.yaml. Without this step, Helm will not download the dependency charts into the charts/ directory, and your deployment will fail with missing template errors. Add helm dependency update as a mandatory step in your CI/CD pipeline before helm upgrade.

Step 9: Package and Publish Your Chart to an OCI Registry

Once your chart is production-ready, you need to distribute it. Helm 4 makes OCI (Open Container Initiative) registries the primary distribution mechanism, alongside traditional chart repositories. OCI registries – Docker Hub, GitHub Container Registry (GHCR), Amazon ECR, and Azure Container Registry – provide authentication, access control, and versioning out of the box, making them superior to the legacy HTTP-based chart repository format.

Package your chart and push it to an OCI registry with the following commands.

# Package the chart into a .tgz archive
helm package ./webapp
# Successfully packaged chart and saved it to: /path/to/webapp-0.1.0.tgz

# Log in to your OCI registry (GitHub Container Registry example)
echo $GITHUB_TOKEN | helm registry login ghcr.io --username YOUR_USERNAME --password-stdin

# Push to OCI registry
helm push webapp-0.1.0.tgz oci://ghcr.io/YOUR_USERNAME/charts

# Output:
# Pushed: ghcr.io/YOUR_USERNAME/charts/webapp:0.1.0
# Digest: sha256:abc123...

# Install directly from OCI registry (on another machine)
helm install my-webapp oci://ghcr.io/YOUR_USERNAME/charts/webapp --version 0.1.0 -n demo

# Show chart info from OCI registry
helm show chart oci://ghcr.io/YOUR_USERNAME/charts/webapp --version 0.1.0

OCI-based distribution simplifies chart management significantly. You no longer need to maintain a separate chart repository server or index file. The chart is stored alongside your container images in the same registry, using the same authentication and access control policies. For teams already using GitHub Container Registry for Docker images, adding Helm charts requires zero additional infrastructure.

Helm 4 also introduces reproducible chart builds, which ensures that packaging the same chart source always produces byte-identical archives. This is critical for supply chain security because it allows you to verify that a chart package was built from a specific commit. Combined with chart signing using helm package --sign, you can establish a complete chain of trust from source code to deployed artifact.

Step 10: Implement Multi-Environment Deployments

Production Helm workflows manage multiple environments – development, staging, and production – from a single chart. The chart templates remain identical; only the values change. This section shows you how to structure your values files and Helm commands for clean multi-environment deployments.

Create environment-specific values files alongside your base values.yaml.

# webapp/values-dev.yaml
replicaCount: 1
image:
 tag: "22-alpine"
resources:
 limits:
 cpu: 250m
 memory: 128Mi
 requests:
 cpu: 50m
 memory: 64Mi
autoscaling:
 enabled: false
ingress:
 hosts:
 - host: webapp.dev.local
 paths:
 - path: /
 pathType: Prefix
configMap:
 LOG_LEVEL: "debug"
# webapp/values-production.yaml
replicaCount: 4
image:
 tag: "22-alpine"
autoscaling:
 enabled: true
 minReplicas: 4
 maxReplicas: 20
 targetCPUUtilizationPercentage: 60
resources:
 limits:
 cpu: 1000m
 memory: 512Mi
 requests:
 cpu: 250m
 memory: 256Mi
ingress:
 hosts:
 - host: webapp.example.com
 paths:
 - path: /
 pathType: Prefix
 tls:
 - secretName: webapp-tls
 hosts:
 - webapp.example.com
configMap:
 LOG_LEVEL: "warn"

Deploy to each environment by passing the appropriate values file. Helm merges environment-specific values on top of the base values.yaml, so you only need to specify the values that differ.

# Deploy to development
helm upgrade --install my-webapp ./webapp 
 -f ./webapp/values-dev.yaml 
 -n dev --create-namespace

# Deploy to staging
helm upgrade --install my-webapp ./webapp 
 -f ./webapp/values-staging.yaml 
 -n staging --create-namespace

# Deploy to production (with secrets from CI/CD)
helm upgrade --install my-webapp ./webapp 
 -f ./webapp/values-production.yaml 
 --set secrets.DATABASE_URL="$PROD_DATABASE_URL" 
 --set secrets.API_KEY="$PROD_API_KEY" 
 -n production --create-namespace --wait --timeout 5m

The --install flag in helm upgrade --install makes the command idempotent – it installs the chart if no release exists, or upgrades it if one does. This is the recommended pattern for CI/CD pipelines because it eliminates the need for conditional logic. The --wait flag blocks until all resources are ready, and --timeout sets a deadline for the deployment to complete.

The following table summarizes the environment configuration differences.

ParameterDevelopmentStagingProduction
Replicas124
AutoscalingDisabled2-5 pods4-20 pods
CPU Limit250m500m1000m
Memory Limit128Mi256Mi512Mi
Log Leveldebuginfowarn
TLSNoNoYes
Ingress Hostwebapp.dev.localwebapp.staging.example.comwebapp.example.com

Step 11: Add Helm Tests and Chart Validation

Helm includes a built-in test framework that runs pod-based tests against your deployed release. Tests are defined as templates in the templates/tests/ directory with the helm.sh/hook: test annotation. When you run helm test, Helm creates the test pods, waits for them to complete, and reports pass or fail based on the exit code.

Replace the default test template with a more thorough one that validates both connectivity and application health.

# webapp/templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
 name: "{{ include "webapp.fullname" . }}-test-connection"
 labels:
 {{- include "webapp.labels" . | nindent 4 }}
 annotations:
 "helm.sh/hook": test
 "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
 containers:
 - name: wget
 image: busybox:1.37
 command: ['sh', '-c']
 args:
 - |
 echo "Testing service connectivity..."
 wget -qO- --timeout=5 http://{{ include "webapp.fullname" . }}:{{ .Values.service.port }}/healthz
 if [ $? -ne 0 ]; then
 echo "FAIL: Could not reach health endpoint"
 exit 1
 fi
 echo "PASS: Health endpoint returned successfully"

 echo "Testing readiness endpoint..."
 wget -qO- --timeout=5 http://{{ include "webapp.fullname" . }}:{{ .Values.service.port }}/ready
 if [ $? -ne 0 ]; then
 echo "FAIL: Could not reach readiness endpoint"
 exit 1
 fi
 echo "PASS: Readiness endpoint returned successfully"
 echo "All tests passed!"
 restartPolicy: Never

# Run tests after deployment:
# helm test my-webapp -n demo
# NAME: my-webapp
# LAST DEPLOYED: Mon Mar 02 10:30:00 2026
# NAMESPACE: demo
# STATUS: deployed
# TEST SUITE: my-webapp-test-connection
# Last Started: Mon Mar 02 11:10:00 2026
# Last Completed: Mon Mar 02 11:10:05 2026
# Phase: Succeeded

The hook-delete-policy: before-hook-creation,hook-succeeded annotation cleans up test pods automatically. Without this, failed test pods remain in the namespace, cluttering your cluster. This is also useful for debugging – when a test fails, the pod stays around so you can inspect its logs with kubectl logs.

Beyond Helm tests, integrate chart validation into your CI/CD pipeline with helm lint, helm template, and tools like kubeval or kubeconform to validate rendered manifests against Kubernetes API schemas. This catches issues like invalid resource kinds, missing required fields, and deprecated API versions before deployment.

Step 12: Set Up Helm Hooks for Lifecycle Management

Helm hooks let you run actions at specific points in the release lifecycle – before install, after upgrade, before delete, and more. Common use cases include running database migrations before a deployment, sending notifications after an upgrade, or cleaning up resources when a release is deleted. Hooks are regular Kubernetes resources with special annotations that tell Helm when to execute them.

Create a pre-upgrade hook that runs database migrations before your application pods are updated.

# webapp/templates/hooks/db-migrate.yaml
apiVersion: batch/v1
kind: Job
metadata:
 name: "{{ include "webapp.fullname" . }}-db-migrate"
 labels:
 {{- include "webapp.labels" . | nindent 4 }}
 annotations:
 "helm.sh/hook": pre-upgrade,pre-install
 "helm.sh/hook-weight": "-5"
 "helm.sh/hook-delete-policy": before-hook-creation
spec:
 template:
 spec:
 containers:
 - name: migrate
 image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
 command: ["node", "migrate.js"]
 envFrom:
 - secretRef:
 name: {{ include "webapp.fullname" . }}-secret
 restartPolicy: Never
 backoffLimit: 3

The hook-weight annotation controls execution order when multiple hooks fire at the same lifecycle point. Lower weights run first. The backoffLimit: 3 allows the migration job to retry up to three times before Helm marks the hook as failed and aborts the upgrade. This prevents a transient database connectivity issue from blocking your deployment permanently.

Common Pitfall #7: Not setting hook-delete-policy on hook resources. Without it, previous hook pods and jobs accumulate in the namespace across upgrades. For migration jobs, use before-hook-creation to delete the old job before creating a new one. For notification hooks that you want to keep for auditing, use hook-succeeded to only clean up successful runs.

Complete Troubleshooting Guide for Kubernetes Helm Deployments

Even with a well-structured helm chart, deployments fail. Kubernetes and Helm have many moving parts, and understanding how to diagnose and fix issues quickly is what separates productive engineers from those stuck in debugging loops. This section covers the eight most common Helm deployment failures, their root causes, and exact commands to fix them.

IssueSymptomsRoot CauseFix
ImagePullBackOffPods stuck in ImagePullBackOff stateWrong image name/tag, missing pull secret, or private registry auth failureVerify image exists: docker pull image:tag. Create imagePullSecret and reference in values.
CrashLoopBackOffPods restart repeatedlyApplication crashes on startup due to missing env vars, bad config, or port conflictCheck logs: kubectl logs pod-name -n demo --previous. Fix env vars or config.
Pending podsPods remain in Pending stateInsufficient cluster resources (CPU/memory) or no matching nodesCheck events: kubectl describe pod pod-name -n demo. Scale cluster or reduce resource requests.
Helm timeoutError: timed out waiting for the conditionPods not reaching Ready state within the timeout periodIncrease timeout: --timeout 10m. Fix underlying pod readiness issue first.
Release stuck in pending-upgradeCannot upgrade or rollbackPrevious upgrade was interrupted (killed terminal, lost connection)Force rollback: helm rollback release-name 0 -n demo (0 = previous working revision).
Template rendering errorError: template: ... unexpected "}" in commandSyntax error in Go templatesValidate locally: helm template my-app ./webapp --debug. Check bracket matching.
Namespace not foundError: namespace "demo" not foundTarget namespace does not existAdd --create-namespace to helm install/upgrade command.
RBAC permission deniedError: forbidden: User cannot create resourceService account or user lacks K8s RBAC permissionsCreate ClusterRoleBinding or RoleBinding for the Helm service account.

Troubleshooting Item 1 – ImagePullBackOff: This is the single most common Helm deployment failure. Run kubectl describe pod POD_NAME -n demo and look at the Events section. If you see β€œFailed to pull image,” verify the image exists in the registry, the tag is correct, and your cluster has the necessary image pull credentials. For private registries, create a Kubernetes Secret of type docker-registry and reference it in your values file under imagePullSecrets.

Troubleshooting Item 2 – CrashLoopBackOff: Your container starts but immediately crashes. Run kubectl logs POD_NAME -n demo --previous to see the logs from the crashed container. Common causes include missing environment variables (the app expects DATABASE_URL but it is not set), permission errors (the container runs as non-root but needs root), or port conflicts (the app listens on port 8080 but the container spec expects 3000).

Troubleshooting Item 3 – Helm template rendering failures: Use helm template my-app ./webapp --debug 2>&1 | head -50 to see exactly where the template engine fails. The --debug flag prints the full error with line numbers. Common causes include missing closing braces, incorrect indentation with nindent, and referencing values that do not exist (typos in .Values.image.tga instead of .Values.image.tag).

Troubleshooting Item 4 – Release stuck in pending-install or pending-upgrade: This happens when a previous Helm operation was interrupted. The release metadata in Kubernetes shows an incomplete state. Fix it with helm rollback RELEASE_NAME 0 -n NAMESPACE, which rolls back to the last successful revision. If rollback also fails, delete the release with helm uninstall RELEASE_NAME -n NAMESPACE and reinstall.

Troubleshooting Item 5 – Values not applying: You updated values.yaml but the deployment does not change. Remember that helm upgrade reads the chart’s values.yaml as defaults, then overlays -f files, then overlays --set flags. If you pass a -f file that does not include a value, Helm uses the chart default – it does not remember values from the previous release. Use --reuse-values to carry forward values from the last release, or always pass the complete values file.

Troubleshooting Item 6 – Dependency version conflicts: When helm dependency update fails with version resolution errors, check that the dependency version in Chart.yaml matches an actual version in the repository. Run helm search repo CHART_NAME --versions to list all available versions. Use version ranges (e.g., ">=16.0.0 <17.0.0") to automatically pick the latest compatible patch release.

Troubleshooting Item 7 – Ingress not routing traffic: If your Ingress resource exists but does not route traffic, verify that an Ingress controller is installed in your cluster. Run kubectl get pods -n ingress-nginx to check. If no controller is running, install one with helm install ingress-nginx ingress-nginx/ingress-nginx -n ingress-nginx --create-namespace. Also verify that the ingressClassName in your chart matches the controller’s class name.

Troubleshooting Item 8 – OCI registry push failures: If helm push fails with authentication errors, verify your registry credentials are current. Run helm registry login REGISTRY_URL to re-authenticate. For GitHub Container Registry, ensure your personal access token has the write:packages scope. For Amazon ECR, refresh your token with aws ecr get-login-password since ECR tokens expire after 12 hours.

Advanced Helm Tips for Production Deployments

Once you have mastered the basics, these advanced techniques will take your Helm deployments to the next level. These tips come from operating Helm charts at scale across hundreds of microservices and multiple Kubernetes clusters.

Use helm diff before every upgrade. The helm-diff plugin shows you exactly what will change before you commit to an upgrade. Install it with helm plugin install https://github.com/databus23/helm-diff and run helm diff upgrade my-webapp ./webapp -n demo. This is invaluable in production where you need to review changes before applying them.

Use Helm 4’s WebAssembly plugin system. Helm 4 replaced the legacy shell-based plugin system with a WebAssembly (Wasm) runtime. Wasm plugins are portable, sandboxed, and significantly faster. Post-renderers – plugins that modify rendered manifests before they are applied – now run as Wasm modules, enabling custom policy enforcement, manifest injection, and validation without shell scripting.

Implement GitOps with Helm. Tools like Flux and Argo CD can reconcile Helm releases from Git repositories automatically. When you push a change to your chart’s values file in Git, the GitOps controller detects the change and triggers a helm upgrade. This eliminates manual deployment steps and provides a complete audit trail through Git history. According to the CNCF 2025 survey, over 60% of organizations using Kubernetes in production have adopted a GitOps workflow.

Use library charts for shared templates. If you maintain multiple microservice charts that share common patterns (standard labels, security contexts, resource templates), extract the shared templates into a library chart. Library charts do not deploy resources directly – they only provide template helpers that other charts can import. This eliminates template duplication across dozens of microservice charts.

Pin dependency versions in production. While version ranges are convenient during development, production charts should pin exact dependency versions (e.g., version: "16.4.1" instead of version: ">=16.0.0"). This prevents unexpected behavior when a dependency releases a breaking change. Use helm dependency update in a controlled process, test the new version in staging, and only then update the pinned version in your production chart.

Helm 4 New Features: What Changed from Helm 3

Helm 4.0.0, released on November 12, 2025, was the most significant Helm release in six years. Understanding the changes helps you take full advantage of the new capabilities and avoid deprecated patterns. The CNCF announced the release alongside Helm’s 10th anniversary celebration, underscoring the project’s importance to the Kubernetes ecosystem. Here is what changed and why it matters for your helm chart workflow.

Server-side apply (SSA) is the headline feature. Helm 3 used client-side three-way merges to compute diffs between the current state, the previous manifest, and the desired manifest. This process was fragile and produced incorrect results when resources were modified outside of Helm (by operators, autoscalers, or manual kubectl commands). SSA delegates the merge to the Kubernetes API server, which understands field ownership and handles conflicts correctly. This single change eliminates the most frustrating class of Helm bugs.

kstatus resource watching replaces the old readiness detection logic. Helm 4 uses the kstatus library to determine when resources are truly ready, not just when the Kubernetes API accepts them. This means --wait is more reliable – it will not prematurely report success when a Deployment’s pods are still starting, and it correctly handles custom resources with status conditions.

Local content-based caching speeds up repeated operations. Helm 4 caches chart downloads, dependency resolution, and template rendering results based on content hashes. If nothing has changed, subsequent helm upgrade commands complete in milliseconds instead of seconds. This is particularly noticeable in CI/CD pipelines that run helm upgrade on every commit.

Helm 3 support timeline: Bug fixes for Helm 3 continue until July 8, 2026, and security fixes until November 11, 2026. After those dates, Helm 3 enters end-of-life. If you are starting new projects, use Helm 4 from day one. If you have existing Helm 3 charts, plan your migration before the security fix deadline. For broader Kubernetes ecosystem updates, see our coverage of Kubernetes 2.0.

Complete Working Project: Full Chart Directory Structure

Here is the complete directory structure for the Helm chart you built in this tutorial. Every file is included with its final contents. You can clone this structure into a new directory and deploy it immediately to any Kubernetes cluster running version 1.28 or later with Helm 4 installed.

webapp/
β”œβ”€β”€ Chart.yaml # Chart metadata and dependencies
β”œβ”€β”€ values.yaml # Default configuration values
β”œβ”€β”€ values-dev.yaml # Development environment overrides
β”œβ”€β”€ values-staging.yaml # Staging environment overrides
β”œβ”€β”€ values-production.yaml # Production environment overrides
β”œβ”€β”€ charts/ # Downloaded dependency charts
β”‚ └── postgresql-16.4.1.tgz # PostgreSQL subchart
β”œβ”€β”€ templates/
β”‚ β”œβ”€β”€ NOTES.txt # Post-install instructions
β”‚ β”œβ”€β”€ _helpers.tpl # Reusable template helpers
β”‚ β”œβ”€β”€ configmap.yaml # ConfigMap resource
β”‚ β”œβ”€β”€ deployment.yaml # Deployment resource
β”‚ β”œβ”€β”€ hpa.yaml # HorizontalPodAutoscaler
β”‚ β”œβ”€β”€ ingress.yaml # Ingress resource
β”‚ β”œβ”€β”€ secret.yaml # Secret resource
β”‚ β”œβ”€β”€ service.yaml # Service resource
β”‚ β”œβ”€β”€ serviceaccount.yaml # ServiceAccount resource
β”‚ β”œβ”€β”€ hooks/
β”‚ β”‚ └── db-migrate.yaml # Pre-upgrade migration job
β”‚ └── tests/
β”‚ └── test-connection.yaml # Helm test pod
└── .helmignore # Files to exclude from packaging

To deploy the complete project from scratch, run these commands in order.

# 1. Create the chart
helm create webapp

# 2. Replace generated files with the templates from this tutorial

# 3. Update dependencies
helm dependency update ./webapp

# 4. Lint and validate
helm lint ./webapp
helm template my-webapp ./webapp --debug > /dev/null

# 5. Deploy to development
helm upgrade --install my-webapp ./webapp 
 -f ./webapp/values-dev.yaml 
 -n dev --create-namespace --wait

# 6. Run tests
helm test my-webapp -n dev

# 7. Verify
kubectl get all -n dev
helm status my-webapp -n dev

# 8. Package and push to registry
helm package ./webapp
helm push webapp-0.1.0.tgz oci://ghcr.io/YOUR_USERNAME/charts

This workflow – create, template, validate, deploy, test, package, publish – is the standard Helm development lifecycle. Automate it in your CI/CD pipeline for consistent, repeatable deployments across every environment. If you are setting up Terraform to provision the underlying Kubernetes infrastructure, our Terraform AWS tutorial covers EKS cluster creation step by step.

Related Coverage

Frequently Asked Questions About Kubernetes Helm Charts

What is Helm and why do I need it for Kubernetes?

Helm is the package manager for Kubernetes. Just as npm manages Node.js packages and pip manages Python packages, Helm manages Kubernetes application packages called charts. Without Helm, deploying a multi-component application to Kubernetes requires manually writing and applying dozens of YAML files for Deployments, Services, ConfigMaps, Secrets, Ingresses, and more. Helm bundles all of these into a single chart with configurable values, making deployments repeatable, upgradeable, and rollback-safe. As of 2026, Helm is used by over 80% of Kubernetes adopters according to the CNCF annual survey.

What is the difference between Helm 3 and Helm 4?

Helm 4, released November 2025, introduces server-side apply for more reliable resource management, a WebAssembly-based plugin system, kstatus resource watching for accurate readiness detection, local content-based caching for faster operations, and slog structured logging. Helm 4 also drops support for Kubernetes 1.15 and earlier. Helm 3 charts are fully compatible with Helm 4 – no migration of chart templates is required. Helm 3 receives bug fixes until July 2026 and security fixes until November 2026.

How do I install Helm on macOS, Linux, or Windows?

On macOS, run brew install helm. On Linux, use the official install script: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash. On Windows, use choco install kubernetes-helm with Chocolatey or scoop install helm with Scoop. Verify the installation by running helm version, which should show v4.1.3 or later as of March 2026.

What is a Helm chart and what does it contain?

A Helm chart is a directory containing everything needed to deploy an application to Kubernetes. It includes Chart.yaml (metadata), values.yaml (default configuration), a templates/ directory with Go template files that render into Kubernetes manifests, and optionally a charts/ directory for dependencies. Charts can be packaged as .tgz archives and published to Helm repositories or OCI registries for distribution.

How do I roll back a failed Helm deployment?

Run helm rollback RELEASE_NAME REVISION_NUMBER -n NAMESPACE. To find available revisions, run helm history RELEASE_NAME -n NAMESPACE. Helm keeps up to 10 revisions by default. Each rollback creates a new revision, preserving the complete deployment history. For stuck releases in pending-install or pending-upgrade state, use helm rollback RELEASE_NAME 0 to revert to the last successful revision.

Can I use Helm with Argo CD or Flux for GitOps?

Yes. Both Argo CD and Flux natively support Helm charts as a source for GitOps-driven deployments. You define your Helm release configuration (chart source, values files, target namespace) in a Git repository, and the GitOps controller continuously reconciles the cluster state with the desired state in Git. This eliminates manual helm upgrade commands and provides an audit trail through Git commits.

How do I publish a Helm chart to an OCI registry?

Package your chart with helm package ./my-chart, authenticate with helm registry login REGISTRY_URL, and push with helm push my-chart-0.1.0.tgz oci://REGISTRY_URL/NAMESPACE. Supported registries include Docker Hub, GitHub Container Registry, Amazon ECR, Azure Container Registry, and Google Artifact Registry. OCI distribution is the recommended method in Helm 4, replacing the legacy chart repository index format.

What is the difference between Helm and Kustomize?

Helm uses Go templates and a values system to generate Kubernetes manifests dynamically. Kustomize uses overlay patches to modify base YAML files without templates. Helm is better for distributing reusable application packages (like database charts) and managing release lifecycle (upgrades, rollbacks, history). Kustomize is simpler for environment-specific patches to existing manifests. Many teams use both: Helm for third-party charts and Kustomize for in-house application configurations. Helm 4’s post-renderer feature allows using Kustomize as a post-processing step on Helm output, combining both approaches.

πŸ‘ Marcus Chen

Marcus Chen

Senior Tech Reporter

Marcus Chen is a Senior Tech Reporter at Tech Insider covering cloud computing, enterprise software, and the business of technology. Before joining TI, he spent five years at ZDNet covering digital transformation across European enterprises and three years at The Register reporting on cloud infrastructure. Marcus is known for his deep dives into cloud cost optimization and multi-cloud strategy. He holds a degree in Computer Science from Imperial College London and speaks regularly at KubeCon and CloudNative events.

View all articles
πŸ‘ Tech Insider
Tech
Insider

Tech Insider delivers in-depth coverage of the technologies shaping the future: AI, cybersecurity, cloud computing, hardware, and the trends that matter.

Company

Explore

Categories

Β© 2026 Tech Insider Media AB. All rights reserved.