Chart.yaml, a values.yaml for defaults, and Go-templated manifests in templates/.helm create mychart scaffolds the full directory structure with best-practice defaults.crds/ cannot be templated -- they are installed as plain YAML before any templates render.apiVersion: v2, name, and version -- all three are required fieldsversion MUST follow SemVer 2.0.0 -- Helm rejects non-compliant versionscrds/ are plain YAML -- Go template directives are not processednamespace: in template metadata -- use {{ .Release.Namespace }} or let users pass --namespace| File / Directory | Required | Purpose | Templated |
|---|---|---|---|
Chart.yaml | Yes | Chart metadata: name, version, apiVersion, dependencies | No |
values.yaml | No (recommended) | Default configuration values for templates | No |
values.schema.json | No | JSON Schema validation for values | No |
templates/ | No | Go-templated Kubernetes manifests | Yes |
templates/_helpers.tpl | No | Named template definitions (partials) | Yes |
templates/NOTES.txt | No | Post-install usage instructions shown to user | Yes |
templates/tests/ | No | Test pod definitions run by helm test | Yes |
charts/ | No | Dependency chart archives (.tgz) or unpacked subdirectories | N/A |
crds/ | No | Custom Resource Definitions (installed before templates) | No |
Chart.lock | No | Locked dependency versions (auto-generated) | No |
.helmignore | No | File patterns to exclude from packaging | No |
LICENSE | No | Chart license file | No |
README.md | No | Chart documentation | No |
| Field | Required | Type | Description |
|---|---|---|---|
apiVersion | Yes | string | v2 for Helm 3/4 (v1 was Helm 2) |
name | Yes | string | Chart name (lowercase, dashes allowed) |
version | Yes | string | Chart version (SemVer 2.0.0) |
type | No | string | application (default, installable) or library (helpers only) |
appVersion | No | string | App version (informational, not used by Helm) |
kubeVersion | No | string | SemVer constraint for compatible K8s versions |
description | No | string | Single-sentence chart description |
dependencies | No | list | Subchart dependencies with name, version, repository |
keywords | No | list | Search keywords |
maintainers | No | list | Maintainer name, email, url |
icon | No | string | URL to SVG/PNG icon |
deprecated | No | bool | Mark chart as deprecated |
START: What do you need?
├── Create a new chart from scratch?
│ ├── YES → Run `helm create mychart` (see Step 1)
│ └── NO ↓
├── Add dependencies/subcharts?
│ ├── YES → Add to Chart.yaml dependencies (see Step 3)
│ │ ├── OCI registry? → Use repository: "oci://registry/path"
│ │ └── Traditional repo? → Use repository: "https://charts.example.com"
│ └── NO ↓
├── Run pre/post-install jobs (DB migration, etc.)?
│ ├── YES → Use Helm hooks with annotations (see Hooks section)
│ └── NO ↓
├── Share reusable template helpers across charts?
│ ├── YES → Create a library chart (type: library)
│ └── NO ↓
├── Publish chart to a registry?
│ ├── OCI registry (recommended) → helm push mychart-1.0.0.tgz oci://registry/repo
│ └── Traditional repo → helm repo index + host index.yaml
└── DEFAULT → Use application chart (type: application)
helm create generates a complete chart directory with best-practice defaults including a Deployment, Service, Ingress, HPA, and ServiceAccount template. [src1]
# Create a new chart named "myapp"
helm create myapp
# Directory structure created:
# myapp/
# ├── Chart.yaml
# ├── values.yaml
# ├── charts/
# ├── templates/
# │ ├── _helpers.tpl
# │ ├── deployment.yaml
# │ ├── hpa.yaml
# │ ├── ingress.yaml
# │ ├── service.yaml
# │ ├── serviceaccount.yaml
# │ ├── NOTES.txt
# │ └── tests/
# │ └── test-connection.yaml
# └── .helmignore
Verify: ls myapp/ → expected: Chart.yaml charts templates values.yaml
Every chart must declare its identity and version. The appVersion field is purely informational. [src1]
# myapp/Chart.yaml
apiVersion: v2
name: myapp
description: A production-ready web application chart
type: application
version: 0.1.0
appVersion: "1.16.0"
kubeVersion: ">=1.22.0-0"
maintainers:
- name: Platform Team
email: [email protected]
keywords:
- web
- api
- microservice
Verify: helm lint myapp/ → expected: 1 chart(s) linted, 0 chart(s) failed
Dependencies are declared in Chart.yaml and resolved with helm dependency update. Both OCI and traditional repositories are supported. [src1] [src5]
# myapp/Chart.yaml (append to existing)
dependencies:
- name: postgresql
version: "15.5.x"
repository: "oci://registry-1.docker.io/bitnamicharts"
condition: postgresql.enabled
- name: redis
version: "19.x.x"
repository: "oci://registry-1.docker.io/bitnamicharts"
condition: redis.enabled
alias: cache
Verify: helm dependency list myapp/ → shows STATUS ok for all dependencies
Structure values with camelCase naming. Prefer flat over nested for simple configs. Quote all strings explicitly to avoid YAML type coercion. [src7]
# myapp/values.yaml
replicaCount: 3
image:
repository: myapp
pullPolicy: IfNotPresent
tag: ""
serviceAccount:
create: true
annotations: {}
name: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls: []
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Verify: helm template myapp myapp/ → renders all YAML with default values substituted
Templates use Go template syntax with Sprig functions. The key built-in objects are .Values, .Release, .Chart, .Files, .Capabilities, and .Template. [src2]
# myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
resources:
{{- toYaml .Values.resources | nindent 12 }}
Verify: helm template myapp myapp/ --debug → shows rendered manifest with no errors
Helm 3.8+ and 4.x support OCI registries natively. The chart name and version are read from Chart.yaml automatically. [src5]
# Package the chart into a .tgz archive
helm package myapp/
# Login to OCI registry
helm registry login ghcr.io -u USERNAME
# Push to OCI registry
helm push myapp-0.1.0.tgz oci://ghcr.io/myorg/charts
# Install directly from OCI
helm install myapp oci://ghcr.io/myorg/charts/myapp --version 0.1.0
Verify: helm show chart oci://ghcr.io/myorg/charts/myapp --version 0.1.0 → displays Chart.yaml contents
# templates/db-migrate-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-db-migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["./migrate", "--up"]
backoffLimit: 3
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string" },
"tag": { "type": "string" },
"pullPolicy": {
"type": "string",
"enum": ["Always", "IfNotPresent", "Never"]
}
}
}
}
}
# BAD -- prevents users from deploying to their chosen namespace
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: production
# GOOD -- namespace set via helm install --namespace or Release.Namespace
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
namespace: {{ .Release.Namespace }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
# BAD -- :latest is mutable, breaks rollbacks and reproducibility
image:
repository: myapp
tag: latest
# GOOD -- pinned version, defaults to Chart.appVersion if empty
image:
repository: myapp
tag: "" # defaults to .Chart.AppVersion via template logic
# BAD -- arrays are hard to override with --set (index-based)
servers:
- name: foo
port: 80
- name: bar
port: 443
# GOOD -- maps are easy to override: --set servers.foo.port=8080
servers:
foo:
port: 80
bar:
port: 443
# BAD -- YAML type coercion: "yes" becomes boolean, "3.0" becomes float
enabled: yes
version: 3.0
# GOOD -- explicit quoting prevents type coercion surprises
enabled: "yes"
version: "3.0"
# BAD -- omitting resources risks OOMKills and noisy-neighbor issues
containers:
- name: myapp
image: myapp:1.0.0
# GOOD -- set sane defaults that users can override via values.yaml
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
resources:
{{- toYaml .Values.resources | nindent 6 }}
include vs template: template does not allow pipeline chaining (e.g., | nindent). Always use include for named templates that need indentation. Fix: replace {{ template "foo" . }} with {{ include "foo" . | nindent N }}. [src2]{{- (trim left) and -}} (trim right) to control whitespace. [src2]Chart.lock is stale. Fix: run helm dependency update to regenerate. [src1]"helm.sh/hook-delete-policy": hook-succeeded annotation. [src3]+ in Kubernetes labels: K8s labels prohibit the + character. Fix: replace + with _ using | replace "+" "_". [src4]appVersion is informational only. Fix: do not rely on it for dependency resolution. [src1]yes/no become booleans; 3.0 becomes a float. Fix: quote all string values. [src7]global: key. [src2]# Lint chart for errors and best-practice warnings
helm lint mychart/
# Render templates locally without installing (dry run)
helm template myrelease mychart/ --debug
# Show computed values (merged defaults + overrides)
helm get values myrelease -n mynamespace
# Show all manifest content of an installed release
helm get manifest myrelease -n mynamespace
# List installed releases across all namespaces
helm list --all-namespaces
# Debug dependency resolution
helm dependency list mychart/
# Validate values against schema
helm install myrelease mychart/ --dry-run --debug
# Show chart metadata from OCI registry
helm show chart oci://ghcr.io/myorg/charts/mychart --version 1.0.0
# Diff upcoming upgrade (requires helm-diff plugin)
helm diff upgrade myrelease mychart/ -f custom-values.yaml
# Run chart test hooks
helm test myrelease -n mynamespace
| Version | Status | Key Changes | Migration Notes |
|---|---|---|---|
| Helm 4.x (Nov 2025) | Current | Server-side apply, WASM plugins, post-renderers as plugins, kstatus-based watching, reproducible builds | Chart.yaml apiVersion v2 unchanged; plugin configs may need updates |
| Helm 3.x (2019-2026) | Bug fixes until Jul 2026, security until Nov 2026 | Tiller removed, per-namespace releases, three-way merge, OCI GA (3.8+) | Most charts work on both 3.x and 4.x |
| Helm 2.x | EOL (Nov 2020) | Required Tiller with cluster-admin | Use helm 2to3 plugin; heritage labels immutable |
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Packaging a multi-resource K8s app for distribution | Simple single-manifest deployment (1-2 YAML files) | Plain kubectl apply or Kustomize |
| Parameterized config across environments (dev/staging/prod) | All environments are identical | kubectl apply with a single manifest |
| Publishing reusable charts to a team or community | One-off internal deployment that never changes | Kustomize overlays |
| Managing complex dependency trees (app + database + cache) | Already using GitOps tool that handles deps | Native GitOps composition |
| Need lifecycle hooks for migrations, backups, or tests | Simple workloads with no pre/post-deploy actions | kubectl apply |
| Distributing charts via OCI registries alongside container images | Vendoring all manifests in a monorepo with no reuse | Directory of plain YAML |
apiVersion: v2 -- existing charts work without changes, but post-renderer plugins may need migrationcrds/ are only installed on helm install -- they are NOT updated on helm upgrade. To update CRDs, apply them separately with kubectl applyindex.yaml -- helm repo add does not work with OCI references; use oci:// prefix directlyhelm uninstall will NOT delete hook-created resources unless a delete-policy is set