Helm Charts: Structure, Templating, Dependencies, Registry
Master Helm charts for Kubernetes: chart structure, templating, values, dependencies, hooks, libraries, registry management, and production patterns for deployment.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Introduction
Helm is the package manager for Kubernetes. You define applications as charts — collections of templated Kubernetes manifests with configurable values. Helm renders templates, manages releases, and handles upgrades and rollbacks. This guide walks through chart structure, templating, values, dependencies, hooks, library charts, registry management, and production patterns.
Chart Structure
my-app/
├── Chart.yaml # Chart metadata (name, version, dependencies)
├── values.yaml # Default configuration values
├── values-prod.yaml # Environment-specific overrides
├── charts/ # Dependency charts (vendored)
├── templates/
│ ├── _helpers.tpl # Named templates (reusable partials)
│ ├── deployment.yaml # Kubernetes Deployment
│ ├── service.yaml # Kubernetes Service
│ ├── ingress.yaml # Kubernetes Ingress
│ ├── configmap.yaml # ConfigMap for app config
│ ├── secret.yaml # Secret (encoded values)
│ ├── hpa.yaml # HorizontalPodAutoscaler
│ ├── pdb.yaml # PodDisruptionBudget
│ ├── serviceaccount.yaml # ServiceAccount + RBAC
│ └── NOTES.txt # Post-install instructions
├── templates/tests/
│ └── test-connection.yaml # Helm test pod
└── .helmignore # Files to exclude from chart package
Chart.yaml
# Chart.yaml
apiVersion: v2
name: my-app
description: A production web application chart
type: application
version: 1.2.3 # Chart version (SemVer)
appVersion: "2.1.0" # Application version
icon: https://example.com/icon.png
home: https://github.com/example/my-app
sources:
- https://github.com/example/my-app
maintainers:
- name: DevOps Team
email: devops@example.com
keywords:
- web
- api
- microservice
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "17.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
Templating
Helper templates
# templates/_helpers.tpl — Reusable named templates
{{- define "my-app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "my-app.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $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 -}}
{{- end -}}
{{- define "my-app.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{ include "my-app.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "my-app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
{{- define "my-app.serviceAccountName" -}}
{{- if .Values.serviceAccount.create -}}
{{- default (include "my-app.fullname" .) .Values.serviceAccount.name -}}
{{- else -}}
{{- default "default" .Values.serviceAccount.name -}}
{{- end -}}
{{- end -}}
Deployment template
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "my-app.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.probes.enabled }}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
{{- end }}
env:
- name: APP_ENV
value: {{ .Values.env | quote }}
{{- range $key, $val := .Values.extraEnv }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- if .Values.volumeMounts }}
volumeMounts:
{{- toYaml .Values.volumeMounts | nindent 12 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Values
Default values
# values.yaml
replicaCount: 2
image:
repository: registry.io/my-app
tag: ""
pullPolicy: IfNotPresent
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 1000
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: app.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: app-tls
hosts:
- app.example.com
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 80
probes:
enabled: true
env: production
extraEnv: {}
postgresql:
enabled: true
auth:
postgresPassword: changeme
database: myapp
redis:
enabled: false
Environment-specific overrides
# values-prod.yaml
replicaCount: 3
image:
tag: "2.1.0"
pullPolicy: IfNotPresent
resources:
limits:
cpu: 1000m
memory: 1Gi
requests:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
tls:
- secretName: app-prod-tls
hosts:
- app.example.com
postgresql:
primary:
persistence:
size: 50Gi
storageClass: fast-ssd
Dependencies
# Update dependencies (downloads to charts/)
helm dependency update my-app/
# Build dependencies (fails if lock file is stale)
helm dependency build my-app/
# List dependencies
helm dependency list my-app/
Conditional dependencies
# In values.yaml — control which dependencies are enabled
postgresql:
enabled: true # Include PostgreSQL subchart
redis:
enabled: false # Skip Redis subchart
Cross-chart value overrides
# Override subchart values from parent
postgresql:
enabled: true
auth:
postgresPassword: "secure-password"
primary:
persistence:
size: 20Gi
image:
tag: "15.4.0"
Hooks
# templates/hooks/post-install-job.yaml — Run after install
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "my-app.fullname" . }}-migrate
labels:
{{- include "my-app.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["npm", "run", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "my-app.fullname" . }}-db
key: url
Hook types
pre-install: Before any resources are created
post-install: After all resources are created
pre-delete: Before any resources are deleted
post-delete: After all resources are deleted
pre-upgrade: Before any resources are updated
post-upgrade: After all resources are updated
pre-rollback: Before any resources are rolled back
post-rollback: After all resources are rolled back
test: When helm test is run
Helm Commands
# Install a chart
helm install my-release my-app/ -f values-prod.yaml -n production
# Upgrade a release
helm upgrade my-release my-app/ -f values-prod.yaml -n production
# Upgrade with atomic rollback on failure
helm upgrade my-release my-app/ -f values-prod.yaml -n production --atomic --timeout 5m
# Dry run (render templates without applying)
helm install my-release my-app/ -f values-prod.yaml --dry-run --debug
# Template rendering only
helm template my-release my-app/ -f values-prod.yaml > rendered.yaml
# List releases
helm list -n production
helm list -A # All namespaces
# Rollback to previous version
helm rollback my-release 3 -n production
# Uninstall
helm uninstall my-release -n production
# Package a chart
helm package my-app/ --version 1.2.3 --destination ./dist/
# Lint a chart
helm lint my-app/
# Run tests
helm test my-release -n production
# Pull from registry
helm pull oci://registry.io/charts/my-app --version 1.2.3
# Push to registry
helm push my-app-1.2.3.tgz oci://registry.io/charts/
Library Charts
# Chart.yaml for library chart
apiVersion: v2
name: common
type: library # Library chart — not deployable
version: 1.0.0
description: Shared templates for application charts
# templates/_deployment.tpl — Define reusable deployment template
{{- define "common.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.name }}
labels:
app: {{ .Values.name }}
spec:
replicas: {{ .Values.replicas | default 2 }}
selector:
matchLabels:
app: {{ .Values.name }}
template:
metadata:
labels:
app: {{ .Values.name }}
spec:
containers:
- name: {{ .Values.name }}
image: {{ .Values.image }}
ports:
- containerPort: {{ .Values.port | default 8080 }}
{{- end -}}
# In application chart — use library
# templates/deployment.yaml
{{ include "common.deployment" . }}
Production Patterns
Multi-environment values
# Directory structure
my-app/
├── values.yaml # Base defaults
├── values-dev.yaml # Dev overrides
├── values-staging.yaml # Staging overrides
├── values-prod.yaml # Production overrides
# Deploy to each environment
helm upgrade --install my-app my-app/ \
-f values.yaml \
-f values-prod.yaml \
--namespace production \
--create-namespace \
--atomic \
--timeout 10m
Secret management with external secrets
# templates/externalsecret.yaml — Use External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "my-app.fullname" . }}
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: {{ include "my-app.fullname" . }}-secret
creationPolicy: Owner
data:
- secretKey: database-url
remoteRef:
key: production/database
property: url
- secretKey: api-key
remoteRef:
key: production/api
property: key
CI/CD with Helm
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: azure/setup-helm@v4
- uses: azure/setup-kubectl@v4
- name: Configure kubectl
run: |
echo "${{ secrets.KUBE_CONFIG }}" | base64 -d > kubeconfig
export KUBECONFIG=kubeconfig
- name: Helm dependency update
run: helm dependency update my-app/
- name: Helm upgrade
run: |
helm upgrade --install my-app my-app/ \
-f my-app/values.yaml \
-f my-app/values-prod.yaml \
--namespace production \
--create-namespace \
--atomic \
--timeout 10m \
--set image.tag=${{ github.sha }}
- name: Helm test
run: helm test my-app -n production
Best Practices
-
For a deeper guide, see Package Kubernetes Manifests with Helm Charts.
-
Use
includeovertemplatefor named templates —includeis flexible and composable -
Use
nindentoverindent— handles leading newlines correctly in YAML -
Set resource requests and limits on every container — prevents resource starvation
-
Use
--atomicflag on production deploys — auto-rollback on failure -
Use
--waitflag — Helm waits for all resources to be ready before marking success -
Version your charts with SemVer —
chartVersionandappVersionare separate -
Use library charts for shared templates — DRY across application charts
-
Use External Secrets Operator for sensitive values — don’t store secrets in values files
-
Lint charts in CI —
helm lintcatches common issues before deployment -
Use
helm templatefor debugging — render templates locally without applying -
Set pod disruption budgets — ensures minimum availability during node drains
-
Use
helm.sh/hook-delete-policy: hook-succeeded— clean up completed hook jobs
Common Mistakes
- Storing secrets in values.yaml: secrets are base64-encoded in templates, not encrypted. Use External Secrets or Sealed Secrets.
- Not using
--atomicin production: a failed upgrade leaves partial resources.--atomicrolls back automatically. - Hardcoding image tags: use
{{ .Values.image.tag }}so CI can set the tag per deployment. - No resource limits: containers without limits can consume all node resources, causing evictions.
- Not pinning dependency versions:
12.x.xallows minor updates which can break. Pin to exact versions for production. - Missing
helm lintin CI: template errors and invalid YAML are caught only at deploy time.
FAQ
What is a Helm chart?
A package of templated Kubernetes manifests. It includes a Chart.yaml with metadata, values.yaml with default configuration, and templates/ with Kubernetes resources rendered from Go templates.
What is the difference between helm install and helm upgrade --install?
helm install creates a new release. helm upgrade --install upgrades if the release exists, or installs if it doesn’t. Use upgrade --install in CI/CD for idempotent deployments.
What are Helm hooks?
Annotations that trigger Jobs or other resources at specific lifecycle points (pre-install, post-upgrade, etc.). Common uses: database migrations (post-install), cleanup jobs (pre-delete), smoke tests (test).
What is a library chart?
A chart with type: library that defines reusable templates but is not deployable itself. Other charts include it as a dependency and call its templates with include. Useful for sharing common patterns across teams.
How do I manage secrets in Helm?
Don’t put secrets in values.yaml. Use External Secrets Operator to sync from Vault/AWS Secrets Manager, or use Sealed Secrets for GitOps workflows. Helm renders secrets as base64-encoded strings, which is not encryption.
Related Resources
GitHub Actions CI/CD: Workflows, Runners, Secrets
Master GitHub Actions for CI/CD: workflows, reusable workflows, composite actions, secrets management, runners, matrix builds, caching, and deployment patterns.
GuideKubernetes Config Management Guide
Master Kubernetes configuration management: ConfigMaps, Secrets, External Secrets Operator, sealed secrets, env injection, volume mounts, and config rotation patterns.
GuideDocker Compose: Multi-Service Local Development
Master Docker Compose for local development: multi-service environments, networking, volumes, profiles, overrides, hot reload, debugging, and production-like setups.