Helm
Packaging, templating and releasing Kubernetes apps with Helm 4 (v4.3). Charts written for
Helm 3 (apiVersion: v2) install unchanged. Objects and kubectl are covered in
Kubernetes.
Concepts
| Term | Meaning |
|---|---|
| Chart | versioned package: Chart.yaml, default values.yaml, templates/ |
| Release | one install of a chart, named and scoped to a namespace |
| Revision | number bumped by every install, upgrade and rollback; stored as Secret sh.helm.release.v1.NAME.vN |
| Values | config tree merged from defaults, -f files and --set flags, then fed to the templates |
| Repository | HTTP server with an index.yaml and .tgz charts; needs helm repo add |
| OCI registry | charts stored as OCI artifacts, addressed as oci://host/path/chart |
| Library chart | type: library: named templates only, renders no objects |
| Subchart | a dependency, vendored into charts/ |
| Hook | object run at a lifecycle point (pre-upgrade, post-install, test, …) |
values.yaml ─┐
-f, --set ──┼─► merge ─► templates/ ─► YAML ─► schema + OpenAPI
charts/ ──┘ check
┌──────────────────────────┘
▼
server-side apply ─► --wait (kstatus) ─► store revisionHelm 4 vs Helm 3
Helm 4.0.0 shipped on 2025-11-12, the first major release in six years. Helm 3 gets bug fixes until 2026-07-08 and security fixes until 2026-11-11.
| Area | Helm 3 | Helm 4 |
|---|---|---|
| Applying objects | client-side three-way merge | server-side apply for new releases; --server-side=auto (upgrade default) keeps a release's previous method |
| Field conflicts | overwritten | reported; --force-conflicts takes ownership |
| Waiting | --wait polls a fixed set of kinds | --wait means watcher (kstatus, any kind); --wait=legacy is the old logic; default hookOnly |
| Roll back on failure | --atomic | --rollback-on-failure (implies --wait=watcher); --atomic is a deprecated alias |
| Force | --force | --force-replace |
| Post-renderers | any executable | must be a plugin: --post-renderer <plugin-name> |
| Plugins | exec only | new system with an optional WebAssembly runtime; CLI, getter and post-renderer types; signature checked by default |
helm registry login | URL accepted | host name only: helm registry login ghcr.io |
| OCI | tag | tag or digest: oci://…/api@sha256:… |
| Chart API | v2 | v2 unchanged; experimental v3 behind HELM_EXPERIMENTAL_CHART_V3=1 |
| Caching, logs | repo cache | content-addressed chart cache; slog logging in the SDK |
| Deprecated | helm template --hide-notes, --render-subchart-notes (removed in v5) |
Releases installed by Helm 3 upgrade in place: the storage format is the same, and they keep
client-side apply until you pass --server-side=true.
CLI
| Command | Does |
|---|---|
helm create api | scaffold a chart (Deployment, Service, Ingress, HTTPRoute, HPA, tests) |
helm install api ./api -n web --create-namespace | first install |
helm upgrade --install api ./api -n web -f values/prod.yaml | install or upgrade; idempotent, use it in CI |
helm rollback api 7 | back to revision 7 (omit the number for the previous one) |
helm uninstall api -n web --keep-history | delete objects, keep history for a later rollback |
helm list -A | releases in all namespaces, any status; filter with --failed, --pending, --deployed |
helm history api | revisions with status, chart, app version and description |
helm status api | state, resources and NOTES |
helm get values api | values you supplied; -a for all computed values |
helm get manifest api --revision 6 | rendered YAML as it was applied |
helm get notes api, get hooks, get metadata | other parts of a release |
helm template api ./api -f values/prod.yaml | render locally, no cluster needed |
helm lint ./api --strict --with-subcharts | static checks; --strict fails on warnings |
helm package ./api --version 1.2.0 --app-version 1.4.2 | build api-1.2.0.tgz |
helm push api-1.2.0.tgz oci://ghcr.io/acme/charts | publish to a registry |
helm pull oci://ghcr.io/acme/charts/api --version 1.2.0 --untar | download and unpack |
helm repo add grafana https://grafana.github.io/helm-charts | register an HTTP repo |
helm repo update | refresh repo indexes |
helm search repo grafana/loki --versions | search added repos; helm search hub for Artifact Hub |
helm dependency update ./api | resolve dependencies into charts/ and write Chart.lock |
helm dependency build ./api | rebuild charts/ exactly from Chart.lock |
helm show values grafana/loki | default values; also show chart, readme, crds, all |
helm test api --logs | run test hooks, print their logs |
helm plugin install, list, update | manage plugins |
| Flag | Use |
|---|---|
-n web, --kube-context prod | namespace and kubeconfig context |
--version 1.2.0 | chart version or range (^1.2); latest if omitted |
-f file.yaml, --set k=v | values (see Values & overrides) |
--wait, --timeout 10m | wait for readiness (kstatus); per-operation timeout, default 5m |
--wait-for-jobs | with --wait, also wait for Jobs to complete |
--rollback-on-failure | failed install is uninstalled, failed upgrade rolled back |
--cleanup-on-fail | delete objects created by a failed upgrade |
--dry-run=server | render with cluster access (lookup, real capabilities), persist nothing |
--reset-then-reuse-values | chart defaults, then last release's values, then your overrides |
--take-ownership | adopt existing objects not created by this release |
--skip-crds, --no-hooks | skip crds/ or hooks |
Chart structure
api/Chart.yaml # metadata and dependenciesChart.lock # pinned dependency versionsvalues.yaml # defaultsvalues.schema.json # JSON Schema for values.helmignore # excluded from the packageREADME.mdcharts/ # dependency .tgz filescrds/ # installed first, never templatedtemplates/_helpers.tpl # named templates; _* never rendereddeployment.yamlservice.yamlingress.yamlNOTES.txt # printed after install and upgradetests/test-connection.yamlapiVersion: v2 # v2 = Helm 3 and 4 charts
name: api
description: Acme API
type: application # or library
version: 1.2.0 # chart SemVer; bump on change
appVersion: "1.4.2" # app version; quote it
kubeVersion: ">=1.33.0-0" # -0 lets pre-releases match
keywords: [api, acme]
home: https://github.com/acme/api
sources: [https://github.com/acme/api]
maintainers:
- name: Acme Platform
email: platform@acme.dev
icon: https://acme.dev/icon.svg
dependencies:
- name: common
version: ~2.3.0
repository: oci://ghcr.io/acme/charts
annotations:
artifacthub.io/license: MITversion is the chart's own version and must change whenever the chart does. appVersion is
informational, conventionally the default image tag. deprecated: true hides a chart from
search.
Values & overrides
replicaCount: 2
image:
repository: ghcr.io/acme/api
tag: "" # empty = .Chart.AppVersion
pullPolicy: IfNotPresent
service:
port: 80
targetPort: 3000
env: []
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 256Mi }
config:
LOG_LEVEL: info
ingress:
enabled: false
className: ""
annotations: {}
hosts: []
tls: []Precedence, lowest to highest (maps merge deeply; lists are replaced whole):
| # | Source |
|---|---|
| 1 | a subchart's own values.yaml |
| 2 | the parent chart's values.yaml (under the subchart's key) |
| 3 | -f / --values files, left to right |
| 4 | --set-json, then --set, then --set-string, then --set-file, then --set-literal |
Flag groups apply in that fixed order whatever their position on the command line; within one flag, later occurrences win.
| Flag | Example | Result |
|---|---|---|
--set | --set image.tag=1.4.2 | nested key; only true, false, null and integers are converted, the rest stays a string |
--set list | --set 'args={serve,--port=3000}' | args: [serve, --port=3000] |
--set index | --set 'env[0].name=MODE,env[0].value=prod' | list item fields |
--set dotted key | --set 'podAnnotations.prometheus\.io/scrape=true' | backslash escapes the dot |
--set null | --set ingress.annotations.foo=null | deletes a key set by defaults |
--set-string | --set-string image.tag=110 | stays "110", not the integer 110 |
--set-json | --set-json 'tolerations=[{"key":"gpu"}]' | JSON values |
--set-file | --set-file appConfig=./app.toml | the file's content as a string |
--set-literal | --set-literal password='a,b=c\d' | no parsing of commas or escapes |
| On upgrade | Values used |
|---|---|
| default | chart defaults + this command's -f/--set only |
--reuse-values | last release's values + overrides; ignores new chart defaults |
--reset-then-reuse-values | new chart defaults, then last release's values, then overrides |
Templating
Templates are Go text/template plus the Sprig library, rendered to YAML before any parsing.
| Object | Holds |
|---|---|
.Values | merged values |
.Release | .Name, .Namespace, .Revision, .IsInstall, .IsUpgrade, .Service (Helm) |
.Chart | Chart.yaml fields, capitalized: .Name, .Version, .AppVersion |
.Capabilities | .KubeVersion.Version, .APIVersions.Has "gateway.networking.k8s.io/v1" |
.Files | non-template files: .Get "conf/app.toml", .Glob "conf/*", .AsConfig, .AsSecrets |
.Template | .Name (current file), .BasePath (the chart's templates dir) |
helm template has no cluster, so .Capabilities is faked; pass --kube-version and
--api-versions to control it.
| Syntax | Means |
|---|---|
{{ .Values.x }} | print a value |
{{- x }}, {{ x -}} | trim whitespace and newlines to the left, right |
{{/* note */}} | comment, prints nothing |
x | f a | pipeline: calls f a x, the piped value goes last |
$v := .Values.x, $v = 2 | declare, reassign a variable |
$ | the root context, reachable inside range and with |
if, else if, else, end | empty is false: false, 0, "", nil, empty list or map |
with .Values.x | rebinds . to x; skipped when x is empty |
range .Values.list | loop; . is the item; range $i, $v := … for index |
range $k, $v := .Values.map | loop over a map, keys sorted |
and, or, not, eq, ne, lt, gt | prefix functions: if and .a (eq .b "x") |
| Function | Example |
|---|---|
default | .Values.image.tag | default .Chart.AppVersion |
required | required "image.repository is required" .Values.image.repository |
quote, squote | {{ .Values.tag | quote }}: always quote strings that look like numbers |
toYaml, nindent | toYaml .Values.resources | nindent 12: newline, then indent |
indent | like nindent without the leading newline |
toJson, fromYaml, fromJson, toToml | convert structures |
include | include "api.labels" . | nindent 4: a named template as a string |
tpl | tpl .Values.hostTemplate .: render a value as a template |
printf | printf "%s-%s" .Release.Name "db" |
trunc, trimSuffix | trunc 63 | trimSuffix "-": DNS-safe names |
lower, upper, replace, trim, contains, hasPrefix | strings |
b64enc, b64dec, sha256sum | encoding and hashing |
dict, list, merge, hasKey, dig, pluck, keys | build and read maps |
ternary | ternary "Always" "IfNotPresent" .Values.dev |
semverCompare | semverCompare ">=1.33-0" .Capabilities.KubeVersion.Version |
lookup | lookup "v1" "Secret" .Release.Namespace "db": live object; empty with helm template |
fail | abort rendering with a message |
include | template | |
|---|---|---|
| Returns | a string you can pipe | writes straight to output |
| nindent 4 | works | impossible |
| Use | always | legacy charts only |
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
containers:
- name: api
env:
{{- range $k, $v := .Values.config }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
- name: RELEASE
value: {{ $.Release.Name }} # $ = root{{- eats the newline before the tag, so a tag on its own line leaves no blank line. Most
"YAML parse error" messages are a wrong nindent count.
Named templates
Names are global across a chart and all its subcharts, so prefix them with the chart name.
{{- define "api.name" -}}
{{- default .Chart.Name .Values.nameOverride
| trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "api.fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name
| trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "api.labels" -}}
{{ include "api.selectorLabels" . }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "api.image" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion }}
{{- printf "%s:%s" .Values.image.repository $tag }}
{{- end }}Selector labels never include the version: selectors are immutable, so a changing label there
breaks every upgrade. To pass more than one argument, build a map:
include "api.container" (dict "root" $ "c" .), then read .root.Values and .c inside.
Hooks & tests
| Hook | Runs |
|---|---|
pre-install, post-install | before any object is created, after all are ready |
pre-upgrade, post-upgrade | around an upgrade |
pre-rollback, post-rollback | around a rollback |
pre-delete, post-delete | around an uninstall |
test | only on helm test |
| Annotation | Values |
|---|---|
helm.sh/hook | comma-separated hook names |
helm.sh/hook-weight | string integer; lower runs first (default "0") |
helm.sh/hook-delete-policy | before-hook-creation (default), hook-succeeded, hook-failed |
helm.sh/resource-policy: keep | not a hook: leave this object behind on uninstall (PVCs, Secrets) |
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "api.fullname" . }}-migrate
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-weight: "0"
helm.sh/hook-delete-policy: >-
before-hook-creation,hook-succeeded
spec:
backoffLimit: 1
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: {{ include "api.image" . }}
command: [bun, run, db:migrate]
envFrom:
- secretRef:
name: {{ .Values.db.existingSecret }}Hooks aren't part of the release's managed objects: helm uninstall leaves them unless a
delete policy removes them. A pre-install hook runs before the chart's own Secrets and
ConfigMaps exist, so reference objects created outside the release (or make those hooks too,
with a lower weight).
apiVersion: v1
kind: Pod
metadata:
name: {{ include "api.fullname" . }}-test
annotations:
helm.sh/hook: test
helm.sh/hook-delete-policy: hook-succeeded
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox:1.37
command: [wget, -qO-]
args:
- http://{{ include "api.fullname" . }}/healthzDependencies & subcharts
dependencies:
- name: postgresql
version: ~16.2.0 # SemVer range
repository: oci://ghcr.io/acme/charts
condition: postgresql.enabled
- name: worker
alias: emails # values key becomes emails:
version: 1.x
repository: file://../worker
tags: [workers]| Field | Does |
|---|---|
repository | https://… repo URL, oci://…, file://../path, or @name of an added repo |
condition | values path that enables the subchart (first one that exists wins) |
tags | enable groups at once: --set tags.workers=false |
alias | install the same chart twice under different keys |
import-values | copy a child's exports or values up into the parent |
global: # all charts: .Values.global
imageRegistry: ghcr.io/acme
postgresql: # the subchart's .Values
enabled: true
auth: { database: app }
emails:
replicaCount: 2A subchart sees only its own key plus global; it can't read the parent's values. Named
templates, though, are shared across all of them. Commit Chart.lock; charts/*.tgz is
usually git-ignored and restored with helm dependency build.
Schema validation
values.schema.json is checked on install, upgrade, lint and template, for the chart
and each subchart. --skip-schema-validation turns it off.
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["image", "service"],
"properties": {
"replicaCount": { "type": "integer", "minimum": 1 },
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": { "type": "string", "minLength": 1 },
"tag": { "type": "string" },
"pullPolicy": {
"enum": ["Always", "IfNotPresent", "Never"]
}
}
},
"service": {
"type": "object",
"properties": {
"port": {
"type": "integer",
"minimum": 1,
"maximum": 65535
}
}
}
}
}"tag": { "type": "string" } catches the classic tag: 1.10 in a values file (a YAML
float) at install time, instead of pulling image api:1.1.
Secrets
| Approach | How | Trade-off |
|---|---|---|
| Values from CI | -f a file or --set-file from the CI secret store | simple; values are in the release Secret, readable by anyone who can read Secrets in the namespace |
| helm-secrets + SOPS | encrypted secrets.yaml in Git; -f secrets://values/secrets.yaml | decrypted at deploy time; age, KMS or PGP keys |
| External Secrets Operator | chart ships an ExternalSecret; ESO pulls from Vault, AWS, GCP, Azure, 1Password | no secret values pass through Helm |
| Sealed Secrets | chart ships a SealedSecret encrypted for the cluster's key | GitOps-friendly; re-seal per cluster |
| Existing Secret | existingSecret: api-db value; chart only references it | secret managed elsewhere |
lookup + randAlphaNum | generate once, reuse the live value on upgrade | breaks with helm template and Argo CD (lookup returns nothing) |
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: {{ include "api.fullname" . }}-db
spec:
refreshInterval: 1h
secretStoreRef: { kind: ClusterSecretStore, name: vault }
target:
name: {{ include "api.fullname" . }}-db
data:
- secretKey: DATABASE_URL
remoteRef: { key: apps/api, property: database_url }OCI registries
echo "$GHCR_TOKEN" | helm registry login ghcr.io \
-u "$GH_USER" --password-stdin
helm package ./api # api-1.2.0.tgz
helm push api-1.2.0.tgz oci://ghcr.io/acme/charts
# stored as ghcr.io/acme/charts/api:1.2.0; prints the digest
helm show values oci://ghcr.io/acme/charts/api \
--version 1.2.0
helm upgrade --install api oci://ghcr.io/acme/charts/api \
--version 1.2.0 -n web
# Helm 4: pin by digest
helm install api \
oci://ghcr.io/acme/charts/api@sha256:9f86d0…| Rule | Detail |
|---|---|
| naming | chart name becomes the repository, chart version the tag |
+ in versions | stored as _ in the tag (tags can't contain +) |
no repo add or search | address charts by full oci:// reference |
| dependencies | repository: oci://ghcr.io/acme/charts (without the chart name) |
| registries | GHCR, ECR, Artifact Registry, ACR, Docker Hub, Harbor, zot |
GitOps & tooling
| Tool | Model | Notes |
|---|---|---|
| helmfile | helmfile.yaml lists releases, values and environments; helmfile diff, helmfile apply | declarative wrapper around the Helm CLI; uses helm-diff |
| Argo CD | Application with source.chart or a chart path plus helm.valueFiles | renders with helm template and applies itself: helm list shows nothing, lookup is empty, hooks map to sync phases |
| Flux | HelmRelease + HelmRepository or OCIRepository | helm-controller runs real Helm installs and upgrades; releases visible to helm list |
| Renovate | bumps chart versions in Chart.yaml, helmfile and Flux or Argo manifests | pairs with any of the above |
repositories:
- name: grafana
url: https://grafana.github.io/helm-charts
releases:
- name: loki
namespace: observability
chart: grafana/loki
version: ~6.40.0
values: [values/loki.yaml]
- name: api
namespace: web
chart: oci://ghcr.io/acme/charts/api
version: 1.2.0
values: [values/prod.yaml]Debugging
| Command | Shows |
|---|---|
helm template api ./api -f v.yaml --debug | render; on a YAML error prints the broken output |
helm template api ./api -s templates/deployment.yaml | one template only |
helm install api ./api --dry-run=server | render with cluster access: real .Capabilities, working lookup; nothing stored |
helm lint ./api --strict | chart and values problems |
helm get manifest api | kubectl diff -f - | drift between the release and the live cluster |
helm history api, helm status api | which revision failed and why |
helm get values api -a --revision 5 | the exact values a revision used |
kubectl get secret -n web -l owner=helm,name=api | release records, one per revision |
| Error | Fix |
|---|---|
another operation (install/upgrade/rollback) is in progress | a run was killed mid-way; helm rollback api to the last deployed revision |
YAML parse error on api/templates/… | indentation: check nindent counts with --debug |
nil pointer evaluating interface {}.x | parent key missing: guard with with, or use dig "a" "b" "" .Values |
exists and cannot be imported into the current release | object made outside Helm: --take-ownership, or add meta.helm.sh/release-name and release-namespace annotations |
field is immutable | selector labels or StatefulSet volumeClaimTemplates changed; delete and recreate the object |
conflict with "kubectl-client-side-apply" | another field manager owns it; fix the source or --force-conflicts |
| value seems ignored | wrong path or subchart key: compare helm get values api -a with helm show values |
image tag 1.1 instead of 1.10 | unquoted 1.10 in YAML is a float: quote it, type it in the schema, | quote in the template |
Recipes
Minimal app chart
A Deployment template built on the helpers above, driven by the values.yaml shown earlier.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "api.fullname" . }}
labels: {{- include "api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "api.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: {{ include "api.image" . }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
env: {{- toYaml .Values.env | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}Conditional Ingress
Render an Ingress only when enabled, with $ to reach the root inside range.
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "api.fullname" . }}
labels: {{- include "api.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations: {{- toYaml . | nindent 4 }}
{{- end }}
spec:
ingressClassName: {{ .Values.ingress.className }}
tls: {{- toYaml .Values.ingress.tls | nindent 4 }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "api.fullname" $ }}
port: { name: http }
{{- end }}
{{- end }}Roll Pods when config changes
A changed ConfigMap doesn't restart Pods; hashing it into a Pod annotation does.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "api.fullname" . }}
data:
{{- range $k, $v := .Values.config }}
{{ $k }}: {{ $v | quote }}
{{- end }} template:
metadata:
annotations:
checksum/config: {{ include
(print $.Template.BasePath "/configmap.yaml") .
| sha256sum }}To restart on every upgrade regardless, use rollme: {{ randAlphaNum 5 | quote }} instead.
Per-environment values files
Keep defaults in values.yaml and only the differences per environment; CI supplies the tag.
# helm upgrade --install api ./api -n prod \
# -f values/prod.yaml --set-string image.tag="$SHA" \
# --rollback-on-failure --timeout 10m
replicaCount: 4
resources:
requests: { cpu: 250m, memory: 512Mi }
limits: { memory: 512Mi }
config:
LOG_LEVEL: warn
ingress:
enabled: true
className: traefik
hosts:
- host: api.acme.dev
tls:
- secretName: api-tls
hosts: [api.acme.dev]Umbrella chart
Install several services as one release with shared globals (helm dependency update shop
first).
apiVersion: v2
name: shop
version: 0.3.0
dependencies:
- name: api
version: ~1.2.0
repository: oci://ghcr.io/acme/charts
- name: web
version: ~2.0.0
repository: file://../web
- name: worker
alias: emails
version: ~1.0.0
repository: oci://ghcr.io/acme/charts
condition: emails.enabledglobal:
imageRegistry: ghcr.io/acme
api:
replicaCount: 3
emails:
enabled: trueRender and diff before upgrading
Review exactly what an upgrade will change, locally or as a CI gate.
# helm-diff; Helm 4 checks the plugin's GPG signature
curl -sL https://github.com/databus23.gpg | gpg --import
u=https://github.com/databus23/helm-diff/releases/latest
helm plugin install \
"$u/download/helm-diff-darwin-arm64.tgz"
# live release vs. new chart + values; exit 2 on changes
helm diff upgrade api ./api -n prod \
-f values/prod.yaml --context 3 --detailed-exitcode
# no cluster: compare two renders
diff -u \
<(helm template api ./api -f values/staging.yaml) \
<(helm template api ./api -f values/prod.yaml)References
- Helm docs (opens in a new tab): the official guide and command reference
- Helm 4 overview (opens in a new tab): what changed from Helm 3, renamed flags
- Helm 4 released (opens in a new tab): release notes and the Helm 3 support timeline
- Chart template guide (opens in a new tab): built-in objects, functions, flow control, named templates
- Charts (opens in a new tab):
Chart.yamlfields, dependencies, schema files - Chart hooks (opens in a new tab) and chart tests (opens in a new tab)
- OCI registries (opens in a new tab): push, pull and install from registries
- Chart best practices (opens in a new tab): naming, values, labels, templates
- Sprig functions (opens in a new tab): the template function library
- Go text/template (opens in a new tab): actions, pipelines and whitespace trimming
- helm-diff (opens in a new tab) and helm-secrets (opens in a new tab): the two plugins most teams install
- External Secrets Operator (opens in a new tab): sync secrets from external stores
- helmfile (opens in a new tab), Argo CD Helm (opens in a new tab), Flux Helm releases (opens in a new tab): GitOps and release orchestration
- Artifact Hub (opens in a new tab): find public charts