Building Helm Charts: From Templates to Production Deployments
In this tutorial, you'll learn about Building Helm Charts: From Templates to Production Deployments. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Helm charts package Kubernetes resources into reusable, versioned, and parameterized bundles that can be deployed across environments with a single command and shared through chart repositories.
What You'll Learn
This tutorial covers scaffolding a chart from scratch, Go template syntax with pipelines and functions, managing subchart dependencies, writing lifecycle hooks for database migrations, testing charts, and publishing to OCI registries.
Why It Matters
Raw YAML files become unmanageable beyond a few Microservices -- every environment requires different values, every team duplicates the same patterns, and rollbacks require manual fixup. Helm charts enforce consistency, enable GitOps workflows, and reduce deployment failures.
Real-World Use
GitLab ships their entire platform as a single Helm chart with over 200 configurable values. Docker uses Helm charts in Docker Desktop to let users deploy complex stacks like WordPress with a single command, managing subcharts for MySQL and Redis automatically.
graph TD A[Chart.yaml] --> B[templates/] A --> C[values.yaml] A --> D[charts/ dependencies] B --> E[deployment.yaml] B --> F[service.yaml] B --> G[_helpers.tpl] B --> H[tests/] C --> I[Helm install -f values.yaml] I --> J[Rendered YAML] J --> K[kubectl apply]
Expected output: diagram showing chart structure -- Chart.yaml, templates, values, and dependencies feeding into the Helm Rendering Pipeline that produces Kubernetes manifests.
Chart Structure
A Helm chart follows a standard directory layout.
helm create mychart
tree mychart/
Expected output:
mychart/
Chart.yaml
values.yaml
charts/
templates/
_helpers.tpl
deployment.yaml
hpa.yaml
ingress.yaml
service.yaml
serviceaccount.yaml
tests/
test-connection.yaml
Chart.yaml
apiVersion: v2
name: webapp
description: A production-ready web application chart
type: application
version: 0.1.0
appVersion: "1.16.0"
kubeVersion: ">=1.25.0-0"
dependencies:
- name: postgresql
version: "12.5.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "18.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
keywords:
- web
- api
- kubernetes
maintainers:
- name: myteam
email: team@example.com
Template Functions and Pipelines
Helm uses Go templates with built-in functions for dynamic YAML generation.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "webapp.fullname" . }}
labels:
{{- include "webapp.labels" . | nindent 4 }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "webapp.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "webapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.port }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
env:
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value | quote }}
{{- end }}
# values.yaml
replicaCount: 3
image:
repository: myapp
tag: ""
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 8080
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 200m
memory: 256Mi
env:
- name: LOG_LEVEL
value: info
- name: NODE_ENV
value: production
ingress:
enabled: true
host: app.example.com
postgresql:
enabled: true
auth:
database: myapp
redis:
enabled: false
Named Templates
Define reusable blocks in _helpers.tpl and include them anywhere.
# templates/_helpers.tpl
{{- define "webapp.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end -}}
{{- define "webapp.labels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
{{- end -}}
{{- define "webapp.selectorLabels" -}}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
# Render templates locally without installing
helm template myrelease ./mychart
# Render with custom values
helm template myrelease ./mychart -f production-values.yaml
# Check for syntax errors
helm lint ./mychart
Expected output: helm template outputs the complete rendered YAML. helm lint reports Chart.yaml: valid and no issues found if everything is correct.
Lifecycle Hooks
Run jobs at specific points in the release lifecycle.
# templates/db-migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "webapp.fullname" . }}-migration
annotations:
"helm.sh/hook": post-install,post-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["/bin/sh", "-c"]
args: ["npm run migrate"]
env:
- name: DB_URL
value: "postgresql://{{ .Release.Name }}-postgresql:5432/{{ .Values.postgresql.auth.database }}"
# Install the chart -- hooks run automatically
helm install myrelease ./mychart
# Check hook execution
kubectl get jobs
kubectl logs job/myrelease-webapp-migration
Expected output: the migration job runs after the release is installed. The job logs show database migration output like "Migration 001_create_users_table completed".
Testing Charts
Write test pods in templates/tests/ to validate deployments.
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "webapp.fullname" . }}-test"
annotations:
"helm.sh/hook": test
spec:
containers:
- name: curl
image: curlimages/curl:latest
command: ['sh', '-c']
args:
- |
echo "Testing endpoint..."
curl -f http://{{ include "webapp.fullname" . }}:{{ .Values.service.port }}/health
restartPolicy: Never
# Run chart tests
helm test myrelease
# Expected output
# POD NAME STATUS
# myrelease-webapp-test success
Expected output: the test pod reports success if the health endpoint returns HTTP 200.
Practice Questions
What is the difference between Chart version and appVersion? Chart version tracks changes to the chart definition and templates. appVersion tracks the application version being deployed. They are independent -- you can update the chart without changing the app.
How do conditionally include a subchart dependency? Use the condition field in Chart.yaml dependencies with a corresponding values.yaml entry. Setting PostgreSQL.enabled: false skips the PostgreSQL subchart during installation.
What command validates a chart without installing it? helm lint checks for YAML syntax errors, missing required fields, and template syntax issues. helm template renders all templates locally for manual review.
Frequently Asked Questions
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro