StackPractices
intermediate By Mathias Paulenko

Deploy Applications to Kubernetes with Helm Charts

Package, version, and deploy Kubernetes applications using Helm charts with value overrides, template functions, and release management for reproducible infrastructure

Package and deploy applications to Kubernetes using Helm, the package manager for K8s. The following demonstrates how to chart structure, templating with values, release upgrades and rollbacks, and dependency management for production-grade deployments.

When to Use This

  • You deploy the same application to multiple environments with different configurations. See Environment Variables for per-environment config.
  • Kubernetes manifests become repetitive and hard to maintain across teams. See Docker Compose Local Dev for local multi-service templates.
  • You need versioned releases with easy rollback capabilities. See Blue-Green Deployment for instant rollback.

Solution

1. Chart Structure

myapp/
  Chart.yaml          # Chart metadata
  values.yaml         # Default configuration values
  values.prod.yaml    # Production overrides
  templates/
    _helpers.tpl      # Named template helpers
    deployment.yaml
    service.yaml
    ingress.yaml
    configmap.yaml
  charts/             # Subchart dependencies

2. Chart Metadata

# Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for MyApp
type: application
version: 1.2.0
appVersion: "2.5.1"
dependencies:
  - name: postgresql
    version: 12.x.x
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

3. Template with Values

# 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 }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          env:
            - name: DATABASE_URL
              value: {{ .Values.database.url | quote }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

4. Default and Override Values

# values.yaml
replicaCount: 2

image:
  repository: myregistry/myapp
  tag: ""
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 3000

ingress:
  enabled: false

database:
  url: "postgres://localhost:5432/myapp"

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 100m
    memory: 128Mi
# values.prod.yaml
replicaCount: 5

ingress:
  enabled: true
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix

resources:
  limits:
    cpu: 2000m
    memory: 2Gi

5. Install and Upgrade

# Install with default values
helm install myapp ./myapp

# Install with production overrides
helm install myapp ./myapp -f values.prod.yaml

# Upgrade existing release
helm upgrade myapp ./myapp -f values.prod.yaml

# Rollback to previous revision
helm rollback myapp 2

# List release history
helm history myapp

6. Named Template Helpers

# templates/_helpers.tpl
{{/* Expand the name of the chart */}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Create a default fully qualified app name */}}
{{- define "myapp.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 }}

How It Works

  • Charts package Kubernetes manifests into versioned, configurable units
  • Templates use Go templating to inject values into YAML resources
  • Values files provide environment-specific configuration overrides
  • Releases track deployed chart versions for easy rollback

Production Considerations

  • Store charts in a registry (Harbor, OCI registry) rather than local files
  • Use helm lint and helm template to validate before deploying
  • Pin dependency versions explicitly to prevent unexpected upgrades

Common Mistakes

  • Hardcoding environment values in templates instead of values files
  • Forgetting to update Chart.version when making changes
  • Not testing helm upgrade before applying to production

Performance Tips

  1. Use helm template for validation. Faster than full deploy:
helm template my-app ./my-app -f values.yaml > manifest.yaml
kubectl apply --dry-run=client -f manifest.yaml
  1. Cache chart dependencies. Avoid re-downloading subcharts:
# Dependencies are cached in charts/ directory
helm dependency build  # Downloads once
helm dependency update  # Only updates changed deps
  1. Use --reuse-values for minor updates. Avoid re-specifying all values:
# Only override specific values, keep the rest
helm upgrade my-app ./my-app --reuse-values --set image.tag=v2.0.1
  1. Parallelize tests with helm test. Run multiple test pods:
# templates/tests/
# test-1.yaml, test-2.yaml, test-3.yaml
# All run in parallel when you execute: helm test my-app

Frequently Asked Questions

How is this different from Kustomize?

Helm uses templating and packaging for reusable charts. Kustomize uses overlays and patches without templating. Helm is better for distributing complex applications; Kustomize is simpler for internal overlays.

Can I use Helm with CI/CD?

Yes. Use helm upgrade --install for idempotent deployments in pipelines. Combine with helm diff to preview changes before applying.