Skip to content
Home
Kubernetes Helm Charts: Package & Deploy Apps at Scale

Kubernetes Helm Charts: Package & Deploy Apps at Scale

Docker Docker 8 min read 1547 words Beginner ExcellentWiki Editorial Team

Helm is the package manager for Kubernetes. Charts package Kubernetes resources into versioned, configurable, and reusable deployment artifacts. Instead of maintaining dozens of YAML files per environment with duplicated configuration, you define Go templates once and customize through values files. This approach, adopted by the Cloud Native Computing Foundation (CNCF) as a graduated project, is used by organizations like Google, Microsoft, and Adobe to manage thousands of Kubernetes service deployments. Helm streamlines the transition from ad-hoc Kubernetes manifests to a managed application platform.

Chart Structure

A Helm chart follows a standard directory layout that the helm create command generates automatically:

my-chart/
  Chart.yaml          # Metadata (name, version, dependencies)
  values.yaml         # Default configuration values
  values.schema.json  # Optional JSON schema for validation
  templates/          # Go template files
    deployment.yaml
    service.yaml
    ingress.yaml
    _helpers.tpl      # Named template definitions
  charts/             # Subchart dependencies (vendored)
  crds/               # Custom Resource Definitions
  README.md

Chart.yaml

The chart metadata file defines the chart’s identity, version, and dependencies:

apiVersion: v2
name: my-app
description: A production-ready web application
version: 0.1.0
appVersion: "1.16.0"
type: application
dependencies:
  - name: postgresql
    version: "~12.1.0"
    repository: https://charts.bitnami.com/bitnami

The apiVersion field distinguishes Helm v2 (v1) from Helm v3 (v2) charts. Helm v3 removed Tiller (the server-side component), simplifying security and RBAC. All new charts should use apiVersion: v2. The Helm documentation provides a complete reference for Chart.yaml fields.

Templates and Values

Helm uses Go templates to generate Kubernetes YAML at deployment time. Values flow from values.yaml (defaults), custom values files (-f production.yaml), and --set flags.

Values File

# values.yaml
replicaCount: 3
image:
  repository: nginx
  tag: stable
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
ingress:
  enabled: true
  host: app.example.com
resources:
  limits:
    cpu: 500m
    memory: 256Mi
  requests:
    cpu: 250m
    memory: 128Mi

Template Example

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-chart.fullname" . }}
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-chart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-chart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.port }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

The template accesses values through .Values, chart metadata through .Chart, and release information through .Release. Built-in objects include .Release.Name, .Release.Namespace, .Release.Service, and .Release.IsUpgrade. Helm’s template documentation covers all built-in objects and their usage.

Named Templates (_helpers.tpl)

Reusable template fragments reduce duplication across multiple template files:

{{- define "my-chart.fullname" -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
{{ include "my-chart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Always include standard Kubernetes recommended labels (app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/managed-by) as defined in the Kubernetes conventions. These labels enable consistent resource selection and grouping across tools like kubectl, monitoring systems, and cost allocation tools.

Chart Hooks

Helm hooks perform actions at specific points in the release lifecycle — before or after install, upgrade, delete, or rollback:

# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-chart.fullname" . }}-migration
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-5"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migration
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["/app/bin/migrate"]

Available hooks: pre-install, post-install, pre-upgrade, post-upgrade, pre-delete, post-delete, pre-rollback, post-rollback, test.

The hook-weight annotation controls execution order (lower weights run first). The hook-delete-policy controls when the hook resource is cleaned up — before-hook-creation ensures a fresh start for each upgrade, and hook-succeeded removes completed hook jobs. Hooks are a critical part of production Helm workflows, enabling zero-downtime database migrations during upgrades.

Managing Dependencies

Charts depend on other charts for common infrastructure components (databases, message queues, monitoring):

# values.yaml — dependency values go under the dependency name
postgresql:
  auth:
    database: myapp
    username: myapp
    password: secret
  primary:
    persistence:
      size: 10Gi
  metrics:
    enabled: true
# Download and vendor dependencies
helm dependency update ./my-chart

# Build dependencies from Chart.lock
helm dependency build ./my-chart

Subchart Value Scoping

Values for subcharts are scoped to the subchart’s name. In the parent chart’s values.yaml, postgresql: values are passed to the postgresql subchart. The subchart receives only its own values block, not the parent’s entire values tree. This scoping prevents conflicts when multiple subcharts are used and is documented in Helm’s chart development guide.

Testing Charts

Helm includes a built-in test framework:

# templates/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "my-chart.fullname" . }}-test"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: curl
      image: curlimages/curl
      command: ['curl']
      args: ['{{ include "my-chart.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never
# Run tests after installation
helm test my-release

Local Validation

# Lint chart for issues
helm lint ./my-chart

# Render templates locally (dry run)
helm template ./my-chart

# Template with custom values
helm template my-release ./my-chart -f production.yaml

# Dry-run install
helm install my-release ./my-chart --dry-run

The helm template command is particularly useful in CI/CD pipelines — it validates template syntax without connecting to a cluster. KodeKloud’s Certified Kubernetes Application Developer (CKAD) course emphasizes this local validation workflow as essential for reliable chart development.

CI/CD Integration

Integrating Helm into CI/CD pipelines ensures that chart changes are validated before deployment. The typical workflow includes linting, template rendering, and optionally deploying to a test cluster for integration testing.

# .github/workflows/helm.yml
jobs:
  lint-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: helm/chart-testing-action@v2
      - name: Run chart-testing (lint)
        run: ct lint --charts charts/*
      - name: Run helm lint
        run: helm lint charts/my-app
      - name: Render templates
        run: helm template my-release charts/my-app
      - name: Install chart in test cluster
        if: github.ref == 'refs/heads/main'
        run: helm upgrade --install my-release charts/my-app --namespace test --create-namespace

For production deployments, use Helm’s --atomic flag to automatically roll back on failure, and pair with ArgoCD or Flux for GitOps workflows that synchronize cluster state with chart versions stored in Git.

Best Practices

Helm charts should follow the same principles as well-designed infrastructure: they should be declarative, versioned, and testable.

  • Use named templates for labels and selectors — ensures consistency
  • Validate values with JSON schema (values.schema.json)
  • Set sensible defaults in values.yaml — make the chart work out of the box
  • Document required values and their behavior in README or schema
  • Pin subchart dependency versions in Chart.yaml
  • Follow semantic versioning for chart versions
  • Use helm lint and helm template in CI before deploying
  • Store charts in a Helm repository (OCI registry, ChartMuseum, or GitHub Pages)

FAQ

What changed between Helm v2 and Helm v3?

Helm v3 removed Tiller (the server-side component), introduced the helm install --create-namespace flag, added OCI registry support for chart storage, improved release management, and simplified security by using the user’s kubeconfig credentials instead of Tiller’s RBAC.

How do I manage secrets with Helm?

Use Helm’s built-in secrets support or external secret management tools. For environment-specific secrets, use --set flags with values from your CI/CD secrets manager, or use SealedSecrets (Bitnami) that encrypts secrets in Git. For production, integrate with HashiCorp Vault or Kubernetes External Secrets operator.

Can I use Helm without a chart repository?

Yes. Helm can install charts from local directories, remote URLs, and OCI registries. Chart repositories simplify discovery and version management but are not required. For teams starting out, keeping charts in the application repository (monorepo style) is simpler and effective.

How do I roll back a failed Helm upgrade?

Use helm rollback my-release REVISION_NUMBER. Specify the revision number to revert to. To see revision history: helm history my-release. Helm v3 supports automatic rollback on failure with helm upgrade --atomic, which rolls back if the deployment fails.

Should I put all infrastructure in one chart or split into multiple?

Split into multiple charts organized by application or functional group (frontend, backend, database). This enables independent versioning, smaller blast radius, and simpler upgrades. Use parent charts (umbrella charts) with dependencies to deploy related services together.

Conclusion

Helm standardizes Kubernetes application packaging and deployment. Charts capture deployment topology in a versioned, shareable artifact. Value files enable environment-specific customization without copying YAML. Hooks manage lifecycle operations like database migrations. Dependencies compose complex applications from reusable components. Adopting Helm transforms Kubernetes from a collection of YAML files into a managed application platform. For teams already using Docker Compose for local development, Helm provides a natural progression to production-grade Kubernetes deployments with the same templating philosophy. For orchestration fundamentals, see our Docker Swarm guide and Docker Compose advanced patterns.

Helm Chart Structure and Best Practices

A Helm chart packages Kubernetes resources. The standard layout: Chart.yaml (metadata), values.yaml (default config), templates/ (Go templates), charts/ (sub-chart deps). Helm’s template language extends Go templates with Sprig functions. Use helper templates (_helpers.tpl) to define reusable named templates. Chart versioning follows SemVer2. Dependencies pull sub-charts automatically. Hooks (pre-install, post-install, pre-upgrade, post-upgrade) run Jobs at lifecycle points. Lint charts with helm lint and package with helm package. Values can be overridden hierarchically: --set image.tag=v2 --values prod-values.yaml. JSON Schema (values.schema.json) validates user-provided values.

Advanced Helm Patterns

Library charts provide helpers without creating release artifacts. Global values propagate to all sub-charts via .Values.global. CI/CD integration: helm template . --values ci-values.yaml to render templates without deploying. OCI-based registries store charts in container registries. Post-renderers pipe templates through Kustomize for environment-specific patches.

Section: Docker 1547 words 8 min read Beginner 756 articles in section Report inaccuracy Back to top