Helm Charts: A Complete Guide to Kubernetes Package Management
Deploying applications to Kubernetes means juggling dozens of YAML manifests — Deployments, Services, ConfigMaps, Secrets, Ingresses, and more. Helm is the package manager that bundles all of those manifests into a single, versioned, configurable unit called a chart. This guide covers everything from chart anatomy to testing and multi-chart orchestration with Helmfile.
Why Helm Exists#
Without Helm you copy-paste manifests across environments, hand-edit image tags, and pray nothing drifts. Helm solves three problems at once:
- Packaging — a chart is a single artifact containing every resource your app needs.
- Templating — Go templates let you inject environment-specific values at install time.
- Lifecycle management —
helm install,helm upgrade, andhelm rollbacktrack revisions so you can undo a bad deploy in seconds.
helm install my-app ./my-chart \
--namespace production \
--values production-values.yaml
Chart Structure#
Every Helm chart follows a standard directory layout:
my-chart/
Chart.yaml # metadata — name, version, appVersion
values.yaml # default configuration values
charts/ # dependency charts (subcharts)
templates/ # Kubernetes manifest templates
deployment.yaml
service.yaml
ingress.yaml
_helpers.tpl # reusable template partials
NOTES.txt # post-install usage instructions
.helmignore # files to exclude from packaging
Chart.yaml#
The Chart.yaml file is the chart's identity card. Key fields include:
- apiVersion —
v2for Helm 3 charts. - name — the chart name, used in repositories.
- version — the chart's SemVer version (bump on every change).
- appVersion — the version of the application being deployed.
- type —
application(default) orlibrary(no rendered manifests). - dependencies — list of required sub-charts.
apiVersion: v2
name: my-app
version: 1.4.0
appVersion: "3.2.1"
type: application
dependencies:
- name: postgresql
version: "12.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
values.yaml — The Configuration Layer#
values.yaml holds every knob your chart exposes. Users override defaults with --set flags or custom values files.
replicaCount: 2
image:
repository: ghcr.io/org/my-app
tag: "3.2.1"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: nginx
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
Value Precedence#
Helm merges values from multiple sources, with later sources winning:
values.yamlin the chart (lowest priority).- Parent chart's
values.yaml(for subcharts). -f/--valuesfiles, in order.--setand--set-stringflags (highest priority).
Templates — Go Templating in Action#
Templates use Go's text/template syntax wrapped in double curly braces. Helm injects a rich set of built-in objects.
Built-in Objects#
.Values— merged values from all sources..Release— release metadata (name, namespace, revision)..Chart— contents ofChart.yaml..Capabilities— cluster API versions and Kubernetes version.
Common Patterns#
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:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: {{ .Values.service.port }}
_helpers.tpl#
Shared template partials live in _helpers.tpl. The define and include functions let you DRY up labels, names, and annotations across every manifest.
{{- define "my-chart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
Conditional and Loop Logic#
Use if, range, and with to control rendering:
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
{{- end }}
Helm Hooks#
Hooks let you run Jobs or other resources at specific points in a release lifecycle. Common hook types:
| Annotation Value | Fires When |
|---|---|
pre-install | Before any release resources are created |
post-install | After all resources are created |
pre-upgrade | Before an upgrade begins |
post-upgrade | After an upgrade completes |
pre-delete | Before a release is deleted |
pre-rollback | Before a rollback starts |
Hook Example — Database Migration#
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-chart.fullname" . }}-migrate
annotations:
"helm.sh/hook": pre-upgrade
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
spec:
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate", "--up"]
restartPolicy: Never
Hook weights control execution order (lower runs first). Delete policies determine cleanup: before-hook-creation, hook-succeeded, or hook-failed.
Chart Dependencies#
Dependencies declared in Chart.yaml are fetched with helm dependency update. They land in the charts/ directory.
Conditional Dependencies#
Use condition or tags to toggle subcharts:
dependencies:
- name: redis
version: "17.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
tags:
- cache
Overriding Subchart Values#
Nest values under the dependency name:
redis:
enabled: true
architecture: standalone
auth:
enabled: false
Chart Repositories#
Repositories are HTTP servers hosting packaged charts (*.tgz) and an index.yaml catalog.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo bitnami/postgresql --versions
OCI Registries#
Helm 3 supports OCI-based registries, letting you push charts to the same registries you use for container images:
helm push my-chart-1.4.0.tgz oci://ghcr.io/org/charts
helm install my-app oci://ghcr.io/org/charts/my-chart --version 1.4.0
Helmfile — Multi-Chart Orchestration#
When your platform has dozens of charts across multiple namespaces, Helmfile brings order. A single helmfile.yaml declares every release, its values, and the deploy order.
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
releases:
- name: postgres
namespace: data
chart: bitnami/postgresql
version: 12.8.0
values:
- postgres-values.yaml
- name: my-app
namespace: production
chart: ./charts/my-app
values:
- production-values.yaml
needs:
- data/postgres
Run helmfile apply to diff and sync every release in dependency order. Use helmfile diff for a dry-run comparison.
Chart Testing#
helm lint#
Catches syntax errors and best-practice violations before you even render templates:
helm lint ./my-chart --values production-values.yaml
helm template#
Renders templates locally without talking to a cluster — perfect for CI pipelines:
helm template my-app ./my-chart --values staging-values.yaml
ct (chart-testing)#
The ct tool from the Helm community automates linting and installation testing across changed charts in a Git repo:
ct lint-and-install --config ct.yaml --charts charts/
Unit Testing with helm-unittest#
Write YAML-based test cases that assert rendered output:
suite: deployment tests
templates:
- deployment.yaml
tests:
- it: should set the correct replica count
set:
replicaCount: 5
asserts:
- equal:
path: spec.replicas
value: 5
Run with helm unittest ./my-chart.
Best Practices#
- Pin dependency versions — use exact versions or tight ranges to avoid surprise upgrades.
- Never hardcode — if a value might change between environments, put it in
values.yaml. - Use _helpers.tpl — centralize labels, names, and annotations.
- Validate with JSON Schema — add a
values.schema.jsonto enforce value types at install time. - Keep hooks idempotent — hooks can run multiple times on retries.
- Sign your charts —
helm package --signadds provenance files for supply-chain security. - Version everything — bump
versionon every chart change, bumpappVersionwhen the app changes.
Codelit publishes in-depth engineering articles every week. This is article #410 in the series — explore more on codelit.io.
Try it on Codelit
GitHub Integration
Paste a repo URL and generate architecture from your actual codebase
Related articles
Try these templates
Headless CMS Platform
Headless content management with structured content, media pipeline, API-first delivery, and editorial workflows.
8 componentsKubernetes Container Orchestration
K8s cluster with pod scheduling, service mesh, auto-scaling, and CI/CD deployment pipeline.
9 componentsProject Management Platform
Jira/Linear-like tool with issues, sprints, boards, workflows, and real-time collaboration.
8 componentsBuild this architecture
Generate an interactive architecture for Helm Charts in seconds.
Try it in Codelit →
Comments