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
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.
Deploy Applications to Kubernetes with Helm Charts
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 lintandhelm templateto 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.versionwhen making changes - Not testing
helm upgradebefore applying to production
FAQ
Q: How is this different from Kustomize? A: 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.
Q: Can I use Helm with CI/CD?
A: Yes. Use helm upgrade --install for idempotent deployments in pipelines. Combine with helm diff to preview changes before applying.
Is this solution production-ready?
Yes. The code examples above show tested implementations. Adapt error handling and configuration to your specific environment before deploying.
What are the performance characteristics?
Performance depends on your data volume and infrastructure. The solutions shown prioritize clarity. For high-throughput scenarios, add caching, batching, and connection pooling as needed.
How do I debug issues with this approach?
Start with the minimal example above. Add logging at each step. Test with small inputs first, then scale up. Use your language’s debugger to step through edge cases.
Helm Subcharts and Dependencies
# Chart.yaml
apiVersion: v2
name: my-app
version: 1.2.0
dependencies:
- name: postgresql
version: 12.12.x
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: 17.11.x
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
# values.yaml
postgresql:
enabled: true
auth:
postgresPassword: "secretpass"
primary:
persistence:
size: 20Gi
redis:
enabled: true
architecture: standalone
auth:
password: "redispass"
# Update dependencies
helm dependency update
# Build dependencies into chart
helm dependency build
# Deploy with subcharts
helm upgrade --install my-app ./my-app -f values.yaml
CI/CD Pipeline with Helm
# .github/workflows/helm-deploy.yml
name: Helm Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/setup-kubectl@v3
- name: Configure Helm
uses: azure/setup-helm@v3
- name: Login to cluster
run: |
echo "${{ secrets.KUBE_CONFIG }}" > kubeconfig
export KUBECONFIG=kubeconfig
- name: Lint chart
run: helm lint ./charts/my-app
- name: Template and diff
run: |
helm template my-app ./charts/my-app -f values-prod.yaml > new.yaml
helm get manifest my-app > current.yaml
diff current.yaml new.yaml || true
- name: Deploy
run: |
helm upgrade --install my-app ./charts/my-app \
-f values-prod.yaml \
--atomic \
--timeout 5m \
--wait
- name: Verify
run: helm test my-app
Helm Secrets with SOPS
# Install helm-secrets plugin
helm plugin install https://github.com/jkroepke/helm-secrets
# Encrypt values file
sops --encrypt --in-place secrets.yaml
# Deploy with encrypted secrets
helm secrets upgrade my-app ./my-app -f secrets.yaml
# The plugin decrypts on-the-fly and never writes plaintext to disk
# secrets.yaml (encrypted with SOPS)
database:
password: ENC[AES256_GCM,data:abc123==,iv:...]
adminToken: ENC[AES256_GCM,data:def456==,iv:...]
Helm Hooks
# templates/hooks/pre-install-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: {{ .Release.Name }}-db-migrate
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
command: ["./migrate", "up"]
# templates/hooks/post-install-test.yaml
apiVersion: v1
kind: Pod
metadata:
name: {{ .Release.Name }}-test
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl
command: ["curl", "-f", "http://{{ .Release.Name }}-service/health"]
Rollback Strategy
# View release history
helm history my-app
# Rollback to previous revision
helm rollback my-app 3
# Rollback with cleanup
helm rollback my-app 3 --cleanup-on-fail
# View values of a specific revision
helm get values my-app --revision 3
# Compare current vs previous
diff <(helm get values my-app --revision 2 -o yaml) \
<(helm get values my-app --revision 3 -o yaml)
Additional Best Practices
- Use
--atomicin production. Roll back automatically on failure:
helm upgrade --install my-app ./my-app \
--atomic \
--timeout 5m \
--wait
- Template before deploying. Catch YAML errors early:
helm template my-app ./my-app -f values.yaml | kubectl apply --dry-run=client -f -
- Use
helm diffto preview changes. See exactly what changes before applying:
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade my-app ./my-app -f values.yaml
Additional Common Mistakes
- Not using
--waitflag. Helm reports success before pods are ready:
# Without --wait: Helm returns immediately
helm upgrade my-app ./my-app
# With --wait: Helm waits for all resources to be ready
helm upgrade my-app ./my-app --wait --timeout 5m
- Overriding subchart values incorrectly. Use the subchart name as prefix:
# Wrong
password: "secretpass"
# Right
postgresql:
auth:
postgresPassword: "secretpass"
- Not cleaning up failed releases. Failed releases consume resources:
# List all releases including failed
helm list --all
# Uninstall failed release
helm uninstall my-app --keep-history # Keep history for audit
Additional FAQ
How do I package and distribute a Helm chart?
# Package chart
helm package ./my-app --version 1.2.0
# Push to OCI registry
helm push my-app-1.2.0.tgz oci://registry.example.com/charts
# Pull from OCI registry
helm pull oci://registry.example.com/charts/my-app --version 1.2.0
How do I manage multiple environments?
Use separate values files per environment:
helm upgrade --install my-app ./my-app -f values.yaml -f values-dev.yaml
helm upgrade --install my-app ./my-app -f values.yaml -f values-staging.yaml
helm upgrade --install my-app ./my-app -f values.yaml -f values-prod.yaml
Should I use Helm or Kustomize?
Use Helm when you need: templating, packaging, dependency management, and distribution. Use Kustomize when you need: simple overlays without templating, and your team prefers plain YAML.
Performance Tips
- Use
helm templatefor validation. Faster than full deploy:
helm template my-app ./my-app -f values.yaml > manifest.yaml
kubectl apply --dry-run=client -f manifest.yaml
- 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
- Use
--reuse-valuesfor 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
- 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 Related Resources
Local Microservices Development with Docker Compose
Orchestrate multi-service local environments with Docker Compose including databases, caches, message brokers, and reverse proxies with hot reload and shared networks
PatternAmbassador Pattern for Resilient Remote Service Access
Add a local ambassador that handles retries, circuit breaking, and monitoring when calling remote services, keeping the client simple and the service logic pure
GuideKubernetes Basics for Application Developers
Learn the core Kubernetes concepts every developer needs: Pods, Services, Deployments, ConfigMaps, and basic kubectl commands.
RecipeObservability Dashboards with Grafana and Prometheus
Build interactive Grafana dashboards that visualize Prometheus metrics with panels, variables, and alerts for thorough service observability
RecipeMetrics Collection and Alerting with Prometheus
Instrument applications and infrastructure with Prometheus metrics, configure alerting rules, and set up recording rules for efficient monitoring of service health
RecipeCloud Cost Optimization
Reduce cloud infrastructure costs with right-sizing, reserved instances, spot instances, and automated resource scheduling across AWS, GCP, and Azure.