Kubernetes Helm Charts: Package & Deploy Apps at Scale
Helm is the package manager for Kubernetes. Charts package Kubernetes resources into versioned, configurable, and reusable deployment artifacts. Instead of maintaining dozens of YAML files per environment with duplicated configuration, you define Go templates once and customize through values files. This approach, adopted by the Cloud Native Computing Foundation (CNCF) as a graduated project, is used by organizations like Google, Microsoft, and Adobe to manage thousands of Kubernetes service deployments. Helm streamlines the transition from ad-hoc Kubernetes manifests to a managed application platform.
Chart Structure
A Helm chart follows a standard directory layout that the helm create command generates automatically:
my-chart/
Chart.yaml # Metadata (name, version, dependencies)
values.yaml # Default configuration values
values.schema.json # Optional JSON schema for validation
templates/ # Go template files
deployment.yaml
service.yaml
ingress.yaml
_helpers.tpl # Named template definitions
charts/ # Subchart dependencies (vendored)
crds/ # Custom Resource Definitions
README.mdChart.yaml
The chart metadata file defines the chart’s identity, version, and dependencies:
apiVersion: v2
name: my-app
description: A production-ready web application
version: 0.1.0
appVersion: "1.16.0"
type: application
dependencies:
- name: postgresql
version: "~12.1.0"
repository: https://charts.bitnami.com/bitnamiThe apiVersion field distinguishes Helm v2 (v1) from Helm v3 (v2) charts. Helm v3 removed Tiller (the server-side component), simplifying security and RBAC. All new charts should use apiVersion: v2. The Helm documentation provides a complete reference for Chart.yaml fields.
Templates and Values
Helm uses Go templates to generate Kubernetes YAML at deployment time. Values flow from values.yaml (defaults), custom values files (-f production.yaml), and --set flags.
Values File
# values.yaml
replicaCount: 3
image:
repository: nginx
tag: stable
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
host: app.example.com
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 250m
memory: 128MiTemplate Example
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-chart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 12 }}The template accesses values through .Values, chart metadata through .Chart, and release information through .Release. Built-in objects include .Release.Name, .Release.Namespace, .Release.Service, and .Release.IsUpgrade. Helm’s template documentation covers all built-in objects and their usage.
Named Templates (_helpers.tpl)
Reusable template fragments reduce duplication across multiple template files:
{{- define "my-chart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}Always include standard Kubernetes recommended labels (app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/managed-by) as defined in the Kubernetes conventions. These labels enable consistent resource selection and grouping across tools like kubectl, monitoring systems, and cost allocation tools.
Chart Hooks
Helm hooks perform actions at specific points in the release lifecycle — before or after install, upgrade, delete, or rollback:
# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-chart.fullname" . }}-migration
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["/app/bin/migrate"]Available hooks: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback, test.
The hook-weight annotation controls execution order (lower weights run first). The hook-delete-policy controls when the hook resource is cleaned up — before-hook-creation ensures a fresh start for each upgrade, and hook-succeeded removes completed hook jobs. Hooks are a critical part of production Helm workflows, enabling zero-downtime database migrations during upgrades.
Managing Dependencies
Charts depend on other charts for common infrastructure components (databases, message queues, monitoring):
# values.yaml — dependency values go under the dependency name
postgresql:
auth:
database: myapp
username: myapp
password: secret
primary:
persistence:
size: 10Gi
metrics:
enabled: true# Download and vendor dependencies
helm dependency update ./my-chart
# Build dependencies from Chart.lock
helm dependency build ./my-chartSubchart Value Scoping
Values for subcharts are scoped to the subchart’s name. In the parent chart’s values.yaml, postgresql: values are passed to the postgresql subchart. The subchart receives only its own values block, not the parent’s entire values tree. This scoping prevents conflicts when multiple subcharts are used and is documented in Helm’s chart development guide.
Testing Charts
Helm includes a built-in test framework:
# templates/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "my-chart.fullname" . }}-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: curl
image: curlimages/curl
command: ['curl']
args: ['{{ include "my-chart.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never# Run tests after installation
helm test my-releaseLocal Validation
# Lint chart for issues
helm lint ./my-chart
# Render templates locally (dry run)
helm template ./my-chart
# Template with custom values
helm template my-release ./my-chart -f production.yaml
# Dry-run install
helm install my-release ./my-chart --dry-runThe helm template command is particularly useful in CI/CD pipelines — it validates template syntax without connecting to a cluster. KodeKloud’s Certified Kubernetes Application Developer (CKAD) course emphasizes this local validation workflow as essential for reliable chart development.
CI/CD Integration
Integrating Helm into CI/CD pipelines ensures that chart changes are validated before deployment. The typical workflow includes linting, template rendering, and optionally deploying to a test cluster for integration testing.
# .github/workflows/helm.yml
jobs:
lint-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: helm/chart-testing-action@v2
- name: Run chart-testing (lint)
run: ct lint --charts charts/*
- name: Run helm lint
run: helm lint charts/my-app
- name: Render templates
run: helm template my-release charts/my-app
- name: Install chart in test cluster
if: github.ref == 'refs/heads/main'
run: helm upgrade --install my-release charts/my-app --namespace test --create-namespaceFor production deployments, use Helm’s --atomic flag to automatically roll back on failure, and pair with ArgoCD or Flux for GitOps workflows that synchronize cluster state with chart versions stored in Git.
Best Practices
Helm charts should follow the same principles as well-designed infrastructure: they should be declarative, versioned, and testable.
- Use named templates for labels and selectors — ensures consistency
- Validate values with JSON schema (
values.schema.json) - Set sensible defaults in
values.yaml— make the chart work out of the box - Document required values and their behavior in README or schema
- Pin subchart dependency versions in
Chart.yaml - Follow semantic versioning for chart versions
- Use
helm lintandhelm templatein CI before deploying - Store charts in a Helm repository (OCI registry, ChartMuseum, or GitHub Pages)
FAQ
What changed between Helm v2 and Helm v3?
Helm v3 removed Tiller (the server-side component), introduced the helm install --create-namespace flag, added OCI registry support for chart storage, improved release management, and simplified security by using the user’s kubeconfig credentials instead of Tiller’s RBAC.
How do I manage secrets with Helm?
Use Helm’s built-in secrets support or external secret management tools. For environment-specific secrets, use --set flags with values from your CI/CD secrets manager, or use SealedSecrets (Bitnami) that encrypts secrets in Git. For production, integrate with HashiCorp Vault or Kubernetes External Secrets operator.
Can I use Helm without a chart repository?
Yes. Helm can install charts from local directories, remote URLs, and OCI registries. Chart repositories simplify discovery and version management but are not required. For teams starting out, keeping charts in the application repository (monorepo style) is simpler and effective.
How do I roll back a failed Helm upgrade?
Use helm rollback my-release REVISION_NUMBER. Specify the revision number to revert to. To see revision history: helm history my-release. Helm v3 supports automatic rollback on failure with helm upgrade --atomic, which rolls back if the deployment fails.
Should I put all infrastructure in one chart or split into multiple?
Split into multiple charts organized by application or functional group (frontend, backend, database). This enables independent versioning, smaller blast radius, and simpler upgrades. Use parent charts (umbrella charts) with dependencies to deploy related services together.
Conclusion
Helm standardizes Kubernetes application packaging and deployment. Charts capture deployment topology in a versioned, shareable artifact. Value files enable environment-specific customization without copying YAML. Hooks manage lifecycle operations like database migrations. Dependencies compose complex applications from reusable components. Adopting Helm transforms Kubernetes from a collection of YAML files into a managed application platform. For teams already using Docker Compose for local development, Helm provides a natural progression to production-grade Kubernetes deployments with the same templating philosophy. For orchestration fundamentals, see our Docker Swarm guide and Docker Compose advanced patterns.
Helm Chart Structure and Best Practices
A Helm chart packages Kubernetes resources. The standard layout: Chart.yaml (metadata), values.yaml (default config), templates/ (Go templates), charts/ (sub-chart deps). Helm’s template language extends Go templates with Sprig functions. Use helper templates (_helpers.tpl) to define reusable named templates. Chart versioning follows SemVer2. Dependencies pull sub-charts automatically. Hooks (pre-install, post-install, pre-upgrade, post-upgrade) run Jobs at lifecycle points. Lint charts with helm lint and package with helm package. Values can be overridden hierarchically: --set image.tag=v2 --values prod-values.yaml. JSON Schema (values.schema.json) validates user-provided values.
Advanced Helm Patterns
Library charts provide helpers without creating release artifacts. Global values propagate to all sub-charts via .Values.global. CI/CD integration: helm template . --values ci-values.yaml to render templates without deploying. OCI-based registries store charts in container registries. Post-renderers pipe templates through Kustomize for environment-specific patches.