Helm Charts for Beginners — Deploying with Charts on k3s and Kubernetes
Why Helm
Without Helm, a typical app needs ~8-15 YAML files (Deployment, Service, Ingress, ConfigMap, Secret, PVC, HPA, ServiceAccount, RBAC...). Values duplicated across files. Updating 5 apps = editing 75 files.
Helm:
- Templated YAML — DRY
- Versioned releases — deploy v1.2, rollback to v1.1 instantly
- Dependencies — "Chart X needs Redis" handled transparently
- Repositories — 5,000+ pre-built charts on Artifact Hub
Install Helm
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
# version.BuildInfo{Version:"v3.14.0"...}Works on any machine that can reach your k3s/K8s cluster.
Pattern 1 — Install existing charts
Add a repository:
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo updateSearch:
helm search repo redis
# NAME CHART VERSION APP VERSION DESCRIPTION
# bitnami/redis 19.5.2 7.2.4 Redis...Install:
helm install mycache bitnami/redis
--namespace cache
--create-namespace
--set auth.password=SuperSecret123Check:
helm list --all-namespaces
kubectl get pods -n cacheUninstall:
helm uninstall mycache -n cacheThat's it — Redis running in K8s with 5 commands.
Pattern 2 — Customise via values.yaml
Charts accept configuration. Instead of --set flags, use a values file:
helm show values bitnami/redis > my-values.yamlEdit my-values.yaml:
architecture: standalone # or "replication" for HA
auth:
password: SuperSecret123
master:
persistence:
size: 10Gi
resources:
limits:
memory: 512Mi
cpu: 500mInstall with values:
helm install mycache bitnami/redis -f my-values.yaml -n cache --create-namespaceKeep my-values.yaml in git. Reproducible deployments.
Pattern 3 — Upgrade in place
# Change something in my-values.yaml
helm upgrade mycache bitnami/redis -f my-values.yaml -n cacheHelm computes the diff, applies only what changed, keeps revision history.
Rollback to previous version:
helm history mycache -n cache
# REVISION UPDATED STATUS CHART
# 1 ... deployed redis-19.5.2
# 2 ... deployed redis-19.5.3
# 3 ... deployed redis-19.5.3 (current)
helm rollback mycache 2 -n cachePattern 4 — Write your own chart
helm create myappCreates:
myapp/
├── Chart.yaml # chart metadata
├── values.yaml # default config
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── serviceaccount.yaml
│ ├── _helpers.tpl # reusable template functions
│ └── NOTES.txt # shown after install
└── charts/ # sub-charts (dependencies)Chart.yaml:
apiVersion: v2
name: myapp
description: My Node.js API
type: application
version: 0.1.0 # chart version
appVersion: "1.0.0" # app versionvalues.yaml:
replicaCount: 2
image:
repository: ghcr.io/yourorg/myapp
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: true
className: traefik
host: myapp.yourcompany.com
tls: true
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
env:
DATABASE_URL: "postgres://..."
JWT_SECRET: "" # set via --set or separate secretstemplates/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 }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 3000
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 10 }}Template language is Go templates with Sprig functions. Common patterns:
# Conditional
{{- if .Values.ingress.enabled }}
...
{{- end }}
# Iterate
{{- range .Values.env }}
- name: {{ .name }}
value: {{ .value }}
{{- end }}
# Include helper
{{- include "myapp.fullname" . }}
# Default values
image: {{ .Values.image.repository | default "busybox" }}Test your chart
# Dry-run — render templates without applying
helm install --dry-run myapp ./myapp
# Diff (requires helm-diff plugin)
helm plugin install https://github.com/databus23/helm-diff
helm diff upgrade myapp ./myapp
# Lint
helm lint ./myappPattern 5 — Secrets management
Never commit secrets in values.yaml.
Option A — External secrets:
Use External Secrets Operator with AWS Secrets Manager / HashiCorp Vault. Your chart references the secret name; actual values live outside git.
Option B — Sealed Secrets:
kubeseal < secret.yaml > sealed-secret.yaml
# sealed-secret.yaml is safe to commit — only cluster can decryptOption C — Manually via `--set`:
helm install myapp ./myapp --set env.JWT_SECRET=$JWT_SECRETOK for CI/CD where secret comes from GitHub Actions env.
Pattern 6 — Dependencies
Chart.yaml:
dependencies:
- name: postgresql
version: "15.5.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "19.5.2"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabledDownload deps:
helm dependency updateInstall with deps:
helm install myapp ./myapp
--set postgresql.enabled=true
--set postgresql.auth.postgresPassword=secret
--set redis.enabled=trueOne chart, whole stack (app + DB + cache).
Pattern 7 — Publish your chart
helm package ./myapp
# Creates myapp-0.1.0.tgz
# Host on GitHub Pages
helm repo index ./charts-repo
# or use ChartMuseum, Harbor, Artifact HubOthers can:
helm repo add yourorg https://yourorg.github.io/charts
helm install myapp yourorg/myappPattern 8 — GitOps with Helm
Argo CD or Flux continuously sync your cluster with Helm charts stored in git.
# Argo CD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
spec:
source:
repoURL: https://github.com/yourorg/myapp
path: chart
targetRevision: main
helm:
valueFiles: [values-prod.yaml]
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: truePush to git → Argo CD detects → applies chart. Always-in-sync cluster.
Common pitfalls
FAQ
Helm — templating, versioning, dependencies, repositories. Kustomize — overlays, no templating, in-tree with kubectl. Helm wins for 3rd-party apps; Kustomize for your own simple overlays.
For 1-2 services — probably not. For 5+ services with shared config — yes.
Yes — helm template renders YAML files you can kubectl-apply manually. But you lose release tracking.
Artifact Hub (discovery), OCI registries (Docker Hub, GHCR, Harbor), Bitnami, self-hosted ChartMuseum, GitHub Pages.
Helm 3 (current) removed Tiller — client-only. Helm 2 is dead. All tutorials should target Helm 3+.
Run Helm on your k3s cluster on DomainIndia VPS. Get a VPS