Server

Helm Chart Anatomy

By Prabath Thalangama· October 24, 2026· 3 min read
#kubernetes#helm#packaging

Introduction

Helm packages Kubernetes manifests as templated, versioned, parameterised charts. A chart is just a directory with a specific layout.

Layout

mychart/
├── Chart.yaml          # name, version, appVersion, dependencies
├── values.yaml         # default configuration
├── values.schema.json  # optional: JSON Schema to validate values
├── templates/
│   ├── _helpers.tpl    # named template definitions (no output on its own)
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── NOTES.txt        # printed after install
│   └── tests/
│       └── test-connection.yaml
├── charts/             # subcharts (dependencies) vendored here
└── .helmignore

Chart.yaml

apiVersion: v2
name: mychart
version: 1.4.2          # the CHART version — bump on any chart change
appVersion: "2.7.0"     # the app version it deploys (informational; often the default image tag)
dependencies:
  - name: postgresql
    version: "15.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

helm dependency update pulls dependencies into charts/.

Templates

Go's text/template with Sprig functions. Built-in objects:

  • .Values — merged values (defaults + -f files + --set)
  • .Release — .Name, .Namespace, .IsInstall, .IsUpgrade, .Revision
  • .Chart — contents of Chart.yaml
  • .Capabilities — cluster version, available API versions
  • .Files — access non-template files in the chart
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "mychart.fullname" . }}
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "mychart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
      labels:
        {{- include "mychart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          {{- with .Values.resources }}
          resources: {{- toYaml . | nindent 12 }}
          {{- end }}

Patterns to know:

  • {{- / -}} trim whitespace (left / right). nindent indents and adds a leading newline.
  • include "name" . calls a named template and returns a string (pipeable); template doesn't return, so prefer include.
  • toYaml . | nindent N renders a values sub-tree as YAML.
  • {{- with .Values.x }} sets . to x inside the block, and skips it if empty.
  • default, required "message" .Values.x, quote, b64enc, tpl (render a string as a template).

_helpers.tpl

{{- define "mychart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}

Hooks

Annotations that run a resource at a lifecycle point:

metadata:
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": hook-succeeded

Common use: a Job that runs DB migrations before the new pods roll.

Lifecycle

helm install myrel ./mychart -f prod-values.yaml
helm upgrade myrel ./mychart -f prod-values.yaml --atomic --timeout 5m
helm upgrade --install myrel ./mychart ...        # idempotent
helm rollback myrel 3
helm uninstall myrel
helm list -A
helm history myrel

Verification and troubleshooting

helm template myrel ./mychart -f prod-values.yaml   # render locally, no cluster
helm lint ./mychart
helm install myrel ./mychart --dry-run --debug
helm get manifest myrel                             # what's actually deployed
helm get values myrel                               # the values used
helm diff upgrade myrel ./mychart -f prod-values.yaml   # (helm-diff plugin)
  • nindent output misaligned / "error converting YAML" — wrong indent count, or used indent (no newline) where nindent was needed, or a {{- ate a newline you needed. helm template and look at the raw output.
  • --set nested value not applied — quoting. --set image.tag=v2 works; values with dots/commas need --set 'a.b=x\,y' or a -f file.
  • Upgrade leaves orphaned resources — Helm only tracks what's in the current manifest; a template you removed is deleted on upgrade, but resources created by hooks or outside Helm aren't. Use --atomic so a failed upgrade rolls back.
  • another operation (install/upgrade/rollback) is in progress — a previous command died. helm rollback to the last good revision, or (last resort) patch out the pending-* status secret.
  • Subchart values not taking — they nest under the subchart name: postgresql: { auth: { password: ... } } in the parent values.yaml.
  • checksum/config didn't trigger a rollout — the referenced template path is wrong; it must resolve to the actual ConfigMap template file.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.