TL;DR

Helm is a Kubernetes package manager and templating tool. For client work, start with a small chart, expose safe knobs in values.yaml, render locally, dry-run against the cluster, then install with helm upgrade --install --wait --atomic.

When To Use Helm

Use Helm when the same Kubernetes workload must be installed repeatedly with different names, environments, images, resources, ingress hosts, storage classes, or feature flags. Avoid Helm for one-off emergency patches where a direct manifest is clearer and the client does not want Helm to own the resource afterward.

NeedHelm fitExample
Install third-party platform toolsExcellentcert-manager, ingress-nginx, external-secrets, prometheus
Deploy internal apps across environmentsExcellentsame app chart with dev, staging, prod values
Generate many similar resourcesGoodone Service and Deployment per app from a values map
Manage drift manually with kubectlPoormanual edits will be overwritten on next Helm upgrade

Install Helm

Helm runs on your laptop, jumpbox, or CI runner. It talks to the cluster using the active kubeconfig context, just like kubectl.

bashinstall-helm.sh
# macOS
brew install helm

# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

helm version
kubectl config current-context
helm list -A

Create A Chart

helm create gives you a working chart. In client work, trim it down so the chart is easy to reason about and only exposes values your team actually supports.

bashnew-chart.sh
helm create web-api
cd web-api

# Common cleanup for a simple app chart.
rm -rf templates/tests
rm -f templates/hpa.yaml templates/serviceaccount.yaml

tree -a .

Minimal Working Chart

This is the smallest useful flow: one Deployment, one Service, one values file, and one helper file. Build this first, then add ingress, config, secrets, PVCs, or autoscaling only when the client needs them.

yamlvalues.yaml
replicaCount: 2

image:
  repository: ghcr.io/example/web-api
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

The helper file defines reusable names and labels for the rest of the chart. Add it early so every Deployment, Service, Ingress, and ConfigMap uses the same naming convention.

gotemplatetemplates/_helpers.tpl
{{- define "web-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{- define "web-api.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name (include "web-api.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

{{- define "web-api.labels" -}}
app.kubernetes.io/name: {{ include "web-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end -}}

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

Deployment And Service

These two templates create the workload and its stable internal network identity. Start with this pair for a stateless web/API service before adding Ingress, autoscaling, config, secrets, or storage.

yamltemplates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "web-api.fullname" . }}
  labels:
    {{- include "web-api.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "web-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "web-api.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ include "web-api.name" . }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: {{ .Values.service.targetPort }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

The Service exposes the Deployment inside the cluster and selects Pods using the same stable labels as the Deployment selector. Keep selector labels immutable after the first install.

yamltemplates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "web-api.fullname" . }}
  labels:
    {{- include "web-api.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  selector:
    {{- include "web-api.selectorLabels" . | nindent 4 }}
  ports:
    - name: http
      port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP

Render And Validate

Do not learn a chart by installing it first. Render it, inspect the YAML, then let the API server validate it with a server-side dry run.

bashrender-validate.sh
helm lint ./web-api --strict
helm template web-api ./web-api -n app-dev -f web-api/values.yaml > /tmp/web-api.yaml
kubectl apply --dry-run=server -f /tmp/web-api.yaml

# Debug template errors with source context.
helm template web-api ./web-api -n app-dev -f web-api/values.yaml --debug

Install, Upgrade, Rollback

Use upgrade --install so the same command works for first install and later changes. --atomic rolls back Kubernetes resources if the upgrade fails, but it cannot undo external side effects such as database migrations.

bashoperate-release.sh
helm upgrade --install web-api ./web-api \
  -n app-dev --create-namespace \
  -f web-api/values.yaml \
  --wait --atomic --timeout 10m

helm status web-api -n app-dev
helm history web-api -n app-dev
kubectl get deploy,svc,pod -n app-dev -l app.kubernetes.io/instance=web-api

# Roll back to a known-good revision during an incident.
helm rollback web-api 3 -n app-dev --wait --timeout 10m

Client Request Flow

Client asks forAdd this to chartCheck after deploy
Expose app inside clusterClusterIP Servicekubectl get endpointslice
Expose app publiclyIngress with TLSIngress address, cert, controller logs
Read app configConfigMap mounted as env or filePod env/file content and rollout checksum
Use password/tokenExisting Secret or ExternalSecret referenceSecret exists; pod has env/mount
Persist dataPVC or StatefulSet volumeClaimTemplatesPVC Bound; volume mounted
Deploy several similar appsRange over .Values.appsOne Deployment/Service per app

See also