version is the chart's own version — bump it when you change templates or defaults. appVersion is the version of the app being deployed (e.g., nginx 1.25). They are independent.
Charts
The anatomy of a Helm chart — Chart.yaml, templates/, values.yaml, helpers, and how all the pieces fit together.
🧒 Simple Explanation (ELI5)
A Helm chart is like a recipe folder. Inside you have: the recipe name and description (Chart.yaml), the default ingredients list (values.yaml), and the cooking instructions with blanks to fill in (templates/). When you "cook" (install) the chart, Helm fills in the blanks with the ingredients and produces the final dish (Kubernetes resources).
🔧 Technical Explanation
Chart Directory Structure
mychart/ ├── Chart.yaml # Chart metadata (name, version, description) ├── Chart.lock # Locked dependency versions ├── values.yaml # Default configuration values ├── values.schema.json # JSON Schema for values validation (optional) ├── templates/ # Kubernetes manifest templates │ ├── deployment.yaml │ ├── service.yaml │ ├── ingress.yaml │ ├── configmap.yaml │ ├── hpa.yaml │ ├── _helpers.tpl # Named template definitions (partials) │ ├── NOTES.txt # Post-install message template │ └── tests/ │ └── test-connection.yaml ├── charts/ # Dependency subcharts (.tgz files) ├── crds/ # Custom Resource Definitions (applied before templates) └── .helmignore # Files to exclude from packaging
Chart.yaml — Metadata
apiVersion: v2 # v2 for Helm 3 (v1 for Helm 2)
name: myapp
description: A web application chart
type: application # "application" or "library"
version: 1.2.0 # Chart version (SemVer)
appVersion: "3.1.0" # App version (informational)
keywords:
- web
- nginx
maintainers:
- name: DevOps Team
email: devops@example.com
dependencies:
- name: postgresql
version: "12.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
values.yaml — Defaults
# values.yaml — sensible defaults for the chart
replicaCount: 2
image:
repository: nginx
tag: "1.25-alpine"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
host: myapp.local
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPU: 70
Template Example
# templates/deployment.yaml
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:
labels:
{{- include "mychart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 80
resources:
{{- toYaml .Values.resources | nindent 12 }}
_helpers.tpl — Named Templates
# templates/_helpers.tpl
{{- define "mychart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end }}
{{- define "mychart.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
{{- end }}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Built-In Objects
| Object | Description | Example |
|---|---|---|
.Values | Values from values.yaml + overrides | .Values.image.tag |
.Chart | Chart.yaml metadata | .Chart.Name, .Chart.Version |
.Release | Release information | .Release.Name, .Release.Namespace |
.Template | Current template info | .Template.Name |
.Capabilities | Cluster capabilities | .Capabilities.KubeVersion |
⌨️ Hands-on
# Scaffold a new chart helm create mychart # Explore the structure ls -la mychart/ cat mychart/Chart.yaml cat mychart/values.yaml cat mychart/templates/deployment.yaml # Render templates locally (no cluster needed) helm template my-release ./mychart # Render with custom values helm template my-release ./mychart --set replicaCount=5 # Lint the chart for errors helm lint ./mychart
🐛 Debugging Scenarios
Scenario 1: Template renders wrong value
# Render and inspect what Helm produces helm template my-release ./mychart --set image.tag=v2.0.0 # If value isn't applied, check: # 1. Is the key path correct? (.Values.image.tag vs .Values.imageTag) # 2. Is there a typo in values.yaml vs template reference? # 3. Did you use --set correctly? (nested: --set image.tag=v2.0.0)
Scenario 2: "Error: template: no template found"
# This happens when: # 1. templates/ directory is empty # 2. Template files have wrong extension (not .yaml or .tpl) # 3. Chart directory structure is wrong # Fix: verify structure ls mychart/templates/ # Should have .yaml files helm lint ./mychart # Will report structural issues
🎯 Interview Questions
Beginner
At minimum: Chart.yaml (metadata), values.yaml (defaults), and templates/ (Kubernetes manifest templates). Optional: charts/ (subcharts), crds/ (CRDs), values.schema.json (validation), _helpers.tpl (named templates), NOTES.txt (post-install message), .helmignore.
Chart.yaml is the metadata file for the chart. Required fields: apiVersion (v2 for Helm 3), name, version (SemVer). Important optional fields: appVersion, description, type (application or library), dependencies, keywords, maintainers.
values.yaml provides default configuration values for the chart. Templates reference these values via {{ .Values.key }}. Users can override defaults with --set key=value or -f custom-values.yaml. It defines the chart's configuration API.
A file in templates/ that defines reusable named templates (partials). Files starting with _ are not rendered as Kubernetes manifests. Common helpers include label generators, name generators, and annotation builders. Referenced with {{ include "mychart.labels" . }}.
A template file in templates/ that is rendered and displayed to the user after helm install or helm upgrade. Used to show access instructions, URLs, next steps. Example: "Get the application URL by running: kubectl port-forward..."
Intermediate
version is the chart's own version — it follows SemVer and should be bumped when templates, values, or chart metadata change. appVersion is the version of the application being deployed (e.g., nginx 1.25). They're independent: you can bump chart version without changing appVersion (e.g., fixing a template bug) or bump appVersion without changing chart version.
.Values (merged values), .Chart (Chart.yaml data), .Release (release info: Name, Namespace, IsInstall, IsUpgrade, Revision, Service), .Template (current template name/basepath), .Capabilities (cluster info: KubeVersion, APIVersions), .Files (access non-template files in the chart).
Two types: application (default) — produces Kubernetes resources, can be installed. library — provides reusable helpers/templates only, cannot be installed directly, must be used as a dependency by application charts. Library charts are for sharing common template patterns across teams.
Custom Resource Definitions. CRDs in crds/ are applied before any templates are rendered, ensuring the CRD types exist in the cluster. Important: CRDs in this directory are only installed, never upgraded or deleted by Helm (to prevent data loss). For CRD lifecycle management, use a separate CRD chart or operator.
Similar to .gitignore, it lists file patterns to exclude when packaging the chart with helm package. Prevents test fixtures, documentation, CI files, etc. from bloating the chart archive. Default excludes: .git, .DS_Store, etc.
Scenario-Based
One chart with environment-specific values files: values-dev.yaml (1 replica, debug logging, no ingress), values-staging.yaml (2 replicas, staging domain), values-prod.yaml (5 replicas, prod domain, HPA enabled, higher resource limits). Deploy: helm install myapp ./mychart -f values-prod.yaml -n prod.
1) Run helm lint ./mychart — read exact error messages and line numbers. 2) Common issues: YAML indentation, missing closing braces {{ }}, referencing undefined values, missing required Chart.yaml fields. 3) Render templates to see output: helm template test ./mychart. 4) Add --debug for more detail. 5) Use helm lint --strict for warnings as errors.
Add it as a dependency in Chart.yaml: dependencies: - name: postgresql, version: "12.x.x", repository: "https://charts.bitnami.com/bitnami". Run helm dependency update. Override PostgreSQL values in your values.yaml under the postgresql: key. Use condition: postgresql.enabled to make it optional.
Define named templates in _helpers.tpl: {{- define "mychart.labels" }} with all standard labels. Use {{ include "mychart.labels" . | nindent 4 }} in every template. Common patterns: fullname, labels, selectorLabels, serviceAccountName. This is the standard practice in the helm create scaffold.
Large files (binaries, test data, node_modules, .git directory) are being included. Fix: 1) Add patterns to .helmignore. 2) Check charts/ — are there unnecessary subcharts? 3) Use helm package --debug to see included files. 4) Move static assets out of the chart (use ConfigMaps or external storage). Charts should typically be under 1MB.
🌍 Real-World Use Case
An e-commerce platform maintains a "base-microservice" chart used by 40+ services:
- Standard chart includes: Deployment, Service, HPA, ConfigMap, Ingress (optional), ServiceMonitor
- Each service has its own
values.yaml— unique image, env vars, resource limits - Chart version is bumped when the platform team updates templates (e.g., adding pod disruption budgets)
- Application teams only manage their values files — they never touch Kubernetes YAML directly
📝 Summary
- A chart is a directory with Chart.yaml, values.yaml, and templates/
Chart.yamldefines metadata: name, version, appVersion, dependenciesvalues.yamlprovides defaults; users override with--setor-f- Templates use Go template syntax with built-in objects:
.Values,.Chart,.Release _helpers.tpldefines reusable named templates to avoid repetition