Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Write Your First Helm Chart

By Kokil Thapa | Last reviewed: September 2026

You need repeatable Kubernetes deploys without copying YAML by hand. That is exactly why teams write your first Helm chart before production traffic hits the cluster. Helm packages manifests into a versioned unit you install, upgrade, and roll back with one command. If you already ship Laravel apps with Deployer on bare metal, this guide bridges that workflow to Kubernetes basics using a small PHP-FPM + Nginx example you can adapt to any web stack.

What do you need before you write your first Helm chart?

Helm 3 is the current line. It talks to the Kubernetes API through your kubeconfig. You do not need Tiller anymore. That change alone removed most Helm 2 security headaches.

Install these before you scaffold anything:

  • kubectl — configured against a reachable cluster (minikube, kind, AKS, EKS, or a staging VPS)
  • Helm 3 — the CLI from the official project (Helm install docs)
  • A container image pushed to a registry your cluster can pull
  • Basic comfort with Deployment, Service, and Ingress objects

On Ubuntu 24.04, a typical install looks like this:

curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
kubectl cluster-info

If you manage servers for clients the way I do with Linux system administration, keep Helm on your laptop or CI runner. Do not install it on every app server. Kubernetes nodes pull images; they do not need the Helm binary unless you deploy from inside the cluster (rare for small teams).

Helm Chart AnatomyChart.yamlname, versionvalues.yamldefaultstemplates/K8s YAMLhelm install / upgradeKubernetes Cluster
How Chart.yaml, values.yaml, and templates combine when you write your first Helm chart and deploy to Kubernetes.

How do you scaffold a new Helm chart from scratch?

Start with the generator. It creates a sensible folder layout you trim down. Do not hand-write every file on day one.

Create the chart directory

mkdir -p ~/charts && cd ~/charts
helm create demo-web
cd demo-web
tree .

You will see Chart.yaml, values.yaml, a templates/ folder, and a charts/ subfolder for dependencies. Delete what you do not need. A minimal web app rarely needs HPA, ServiceAccount boilerplate, or the default nginx example on first pass.

Edit Chart.yaml metadata

apiVersion: v2
name: demo-web
description: Demo web app Helm chart
type: application
version: 0.1.0
appVersion: "1.0.0"

version tracks chart changes. appVersion tracks the application release inside the container. Bump version on every chart edit you ship. CI pipelines often enforce semver here. Read Helm charts explained for how packaging differs from a raw manifest folder.

Define values.yaml defaults

Values are your configuration surface. Keep secrets out of this file. Use Kubernetes Secrets or an external secret manager instead.

replicaCount: 2

image:
  repository: registry.example.com/demo-web
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: demo.example.com
      paths:
        - path: /
          pathType: Prefix

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

Paste that into a JSON formatter only when you convert between JSON and YAML for CI. Helm itself expects YAML.

How do you write Helm templates for Deployment, Service, and Ingress?

Templates are standard Kubernetes manifests with Go template directives. Helm renders them at install time. The dot (.) refers to the values tree passed in.

Deployment template

Save this as templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "demo-web.fullname" . }}
  labels:
    {{- include "demo-web.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "demo-web.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "demo-web.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: 8080
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

Notice include helpers in templates/_helpers.tpl. Keep them. They prevent label drift across objects. That drift breaks selectors silently.

Service and Ingress

Add templates/service.yaml exposing port 80 to pod port 8080. Wrap Ingress in a values guard so staging clusters without an ingress controller still work:

{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "demo-web.fullname" . }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  rules:
    {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
          {{- range .paths }}
          - path: {{ .path }}
            pathType: {{ .pathType }}
            backend:
              service:
                name: {{ include "demo-web.fullname" $ }}
                port:
                  number: {{ $.Values.service.port }}
          {{- end }}
    {{- end }}
{{- end }}

Run helm template demo . locally. It prints rendered YAML without touching the cluster. That command saves hours when a template syntax error would otherwise fail mid-deploy. For larger teams, pair this with Azure Pipelines CI/CD or CircleCI pipelines that lint charts on every push.

Helm Template Render Flowvalues.yaml+ overridesGo templatestemplates/*.yamlRendered YAMLkubectl applyCommon template functions{{ .Values.replicaCount }} | {{- if .Values.ingress.enabled }}include "chart.fullname" . | toYaml | nindent
Helm merges values with Go templates to produce final Kubernetes manifests during install or upgrade.

How do you install, upgrade, and roll back your first Helm release?

A release is a named install of a chart in a namespace. One chart can produce many releases (staging, QA, prod).

Install the chart

  1. Create a namespace: kubectl create namespace demo
  2. Dry-run first: helm install demo-release . -n demo --dry-run --debug
  3. Install for real: helm install demo-release . -n demo
  4. Verify: helm list -n demo and kubectl get pods -n demo

Override values without editing files:

helm install demo-release . -n demo \
  --set replicaCount=3 \
  --set image.tag=1.0.1 \
  -f values-staging.yaml

Upgrade and rollback

Ship a new image tag by bumping values.yaml and running upgrade:

helm upgrade demo-release . -n demo --set image.tag=1.0.2
helm history demo-release -n demo
helm rollback demo-release 1 -n demo

Each upgrade creates a revision. Rollback re-applies a prior revision's rendered manifests. That is the operational win over plain kubectl apply folders. On a booking platform like Adventure Third Pole Trek, zero-downtime deploys still depend on readiness probes and rolling update strategy inside the Deployment template—not Helm alone.

Helm Release LifecycleRev 1 InstallRev 2 UpgradeRev 3 UpgradeRollbackto Rev 1helm history shows every revisionhelm rollback <release> <revision>
Each helm upgrade creates a numbered revision you can inspect and roll back when a deploy misbehaves.

How does Helm compare to raw YAML and Kustomize?

Teams often ask whether Helm is worth the template layer. The honest answer depends on release count and environment variance.

ApproachBest forUpgrade / rollbackLearning curvePackage sharing
Raw YAML + kubectlOne app, one cluster, few envsManual or scriptedLowCopy-paste repos
KustomizeOverlay patches per envGit-based, no built-in historyMediumBase + overlay folders
Helm chartsMany envs, shared apps, CI pipelinesBuilt-in revision historyMedium–highChart repos, OCI registries

Kustomize ships inside kubectl. It excels when differences are patch-shaped ("add three replicas in prod"). Helm excels when differences are parameter-shaped ("image tag, host name, replica count"). Many production setups use both: a Helm chart with environment-specific values files. If you deploy to Azure Kubernetes Service, Helm integrates cleanly with pipeline tasks and OCI artifact storage.

For AI-assisted YAML generation, see using AI to write Terraform and Kubernetes YAML. Always run helm template and kubeconform or helm lint before merging. Models hallucinate API versions often.

What mistakes break your first Helm chart in production?

These show up on real clusters, not just tutorials.

Hard-coded names instead of helpers

Never set metadata.name: demo-web literally. Use {{ include "demo-web.fullname" . }}. Otherwise a second release in the same namespace collides.

Secrets in values.yaml committed to Git

Store DB passwords in Kubernetes Secrets. Reference them from templates with secretKeyRef. For enterprise clients, wire enterprise application development practices: sealed-secrets, External Secrets Operator, or cloud vault integration.

Missing resource limits

Without limits, one pod can starve a node. Set requests and limits in values. Tune them from actual usage, not guesses.

Forgetting hooks and CRD order

Database migration Jobs often need helm.sh/hook: pre-install,pre-upgrade. Custom Resource Definitions must install before controllers that consume them. Read the Kubernetes objects overview when ordering confuses you.

Skipping lint and dry-run in CI

helm lint .
helm template demo-release . -f values-staging.yaml | kubeconform -summary -

Add that step beside unit tests. A broken Ingress template should fail the pipeline, not the production namespace. Support and maintenance contracts get expensive when every deploy is a manual kubectl rescue.

First Helm Chart ChecklistChart ready?helm lint passfix templatesdry-run OKhelm templatehelm installwatch podscheck valuesno secrets in Git
Run lint and dry-run before your first helm install—catch template errors before they hit the cluster.

Subcharts before you need them

Do not split database and app into subcharts on day one. Add a charts/postgresql dependency only when multiple apps share it. Until then, one flat chart stays easier to debug. Validate regex in host patterns with a regex tester if you template hostnames dynamically.

When admission policies enforce labels or security contexts, your chart must comply. See admission controllers and webhooks for what cluster admins inject automatically.

How do you publish and reuse your Helm chart?

Once the chart works locally, package it for teammates and CI.

helm package .
helm push demo-web-0.1.0.tgz oci://registry.example.com/charts

Helm 3 supports OCI registries alongside classic chart repos. Tag chart versions the same way you tag Docker images. Document required values in a README inside the chart folder.

On client projects I treat chart repos like Git deploy branches. Staging installs chart version 0.3.x. Production pins an exact patch after QA passes. That mirrors Deployer release symlinks I use on PHP sites—predictable, reversible change.

If your app exposes a REST API, keep deploy config near API versioning docs. API-first development workflow pairs well with chart-per-service repos in larger systems built through custom software development.

For a broader platform picture, browse the portfolio of production systems. Charts are plumbing. The business outcome is still uptime and safe releases.

Key Takeaways

  • Run helm create, trim boilerplate, and keep helpers in _helpers.tpl for consistent names and labels.
  • Put all environment differences in values.yaml or -f override files—never hard-code prod hostnames in templates.
  • Always helm lint and helm template --debug before helm install to catch render errors early.
  • Use helm upgrade for releases and helm rollback when a revision misbehaves—revision history is Helm's core advantage.
  • Keep secrets out of Git; reference Kubernetes Secret objects from templates instead.
  • Package charts with helm package and push to an OCI registry once staging proves the chart stable.

People Also Ask

Do I need Helm if I already use kubectl apply?

Not always. A single static manifest folder works for hobby projects. Helm pays off when you manage multiple environments, share charts across services, or need one-command rollbacks. The template plus values split removes copy-paste YAML drift.

What is the difference between a Helm chart and a Helm release?

The chart is the package—the files in your repo. A release is a named instance of that chart running in a cluster namespace. Installing the same chart twice with different release names creates two independent sets of resources.

Can Helm deploy Laravel or PHP applications?

Yes. Build a Docker image with PHP-FPM or Octane, point the chart Deployment at that image, and front it with Nginx in the same pod or a sidecar. Helm does not care about the language. It renders Kubernetes objects. Your Dockerfile and health checks matter more than the chart tool.

Is Helm still maintained in 2026?

Yes. Helm 3 remains the standard packaging layer for Kubernetes applications. The CNCF graduated project ships regular releases. OCI support and security signing continue to improve. Check the official site for the current stable version before pinning CI images.

Ship your first chart, then automate the pipeline

You now have the full path to write your first Helm chart: scaffold with helm create, configure values, template Deployment and Ingress resources, install, upgrade, and roll back with confidence. Start on a staging cluster, wire lint into CI, and only then promote the same chart to production with pinned versions. If you want help moving a Laravel or multi-service app from VPS deploys to Kubernetes, contact us or explore web development services for a practical migration plan.

Frequently Asked Questions

Install Helm 3 and kubectl with a kubeconfig pointing at a reachable cluster—minikube, kind, AKS, EKS, or a staging VPS. Push a container image to a registry your cluster can pull. You should be comfortable with Deployment, Service, and Ingress objects. Helm 3 talks to the Kubernetes API directly; Tiller is gone. On Ubuntu 24.04, install via the official get-helm-3 script, then verify with helm version and kubectl cluster-info. Keep Helm on your laptop or CI runner, not on every app server.

Start with the generator, not hand-written files. Run helm create demo-web inside a charts directory. You get Chart.yaml, values.yaml, templates/, and charts/ for dependencies. Delete boilerplate you do not need—HPA, ServiceAccount defaults, or the nginx example on a first pass. Edit Chart.yaml with apiVersion v2, name, version, and appVersion. Define replica count, image, service, ingress, and resources in values.yaml. Bump chart version on every edit you ship.

Run helm create my-app, edit Chart.yaml and values.yaml, add Go-template manifests under templates/, then install with helm install and upgrade with helm upgrade.

Chart.yaml holds packaging metadata—name, chart version, and appVersion tracking the container release. values.yaml is your configuration surface for replica count, image tag, ingress hosts, and resource limits; keep secrets out of it. Templates under templates/ are standard Kubernetes manifests with Go template directives where the dot refers to the values tree. Helm merges values with templates at install or upgrade time to produce final manifests you would otherwise copy by hand.

Save Deployment with replicas from .Values.replicaCount, image from .Values.image.repository and tag, and resources via toYaml. Keep include helpers in templates/_helpers.tpl for names and labels—hard-coded metadata.name causes collisions when a second release lands in the same namespace. Add Service exposing port 80 to containerPort 8080. Wrap Ingress in {{- if .Values.ingress.enabled -}} so staging clusters without an ingress controller still install cleanly. Run helm template demo . locally to print rendered YAML without touching the cluster.

A release is a named install of a chart in a namespace. Create the namespace with kubectl create namespace demo. Dry-run first: helm install demo-release . -n demo --dry-run --debug. Install for real with helm install demo-release . -n demo. Verify with helm list -n demo and kubectl get pods -n demo. Override values without editing files using --set replicaCount=3 --set image.tag=1.0.1 or -f values-staging.yaml. One chart can produce many releases across staging, QA, and production.

Run helm history demo-release -n demo, then helm rollback demo-release 1 -n demo to re-apply a prior revision.

The chart is the package—the files in your repo. A release is a named instance of that chart running in a cluster namespace.

Raw YAML plus kubectl apply suits one app on one cluster with few environments—low learning curve, but upgrades are manual. Kustomize ships inside kubectl and excels at overlay patches per environment, yet has no built-in revision history. Helm excels when differences are parameter-shaped—image tags, host names, replica counts—and you manage many environments, shared apps, or CI pipelines needing one-command rollbacks. Many production setups use both: a Helm chart with environment-specific values files. If you deploy to Azure Kubernetes Service, Helm integrates with pipeline tasks and OCI artifact storage.

Not always. A single static manifest folder works for hobby projects. Helm pays off when you manage multiple environments, share charts across services, or need one-command rollbacks. The template plus values split removes copy-paste YAML drift between staging and production. For a lone Deployment on one cluster, kubectl apply stays simpler. Once environment variance grows, Helm's revision history and values overrides save more time than the template layer costs.

Yes. Build a Docker image with PHP-FPM or Octane, point the chart Deployment at that image, and front it with Nginx in the same pod or as a sidecar. Helm does not care about the language—it renders Kubernetes objects. Your Dockerfile, health checks, and readiness probes matter more than the chart tool. If you already ship Laravel with Deployer on bare metal, this workflow bridges that pattern to Kubernetes. Zero-downtime deploys still depend on readiness probes and rolling update strategy inside the Deployment template, not Helm alone.

Yes. Helm 3 remains the standard Kubernetes packaging layer.

Hard-coded names like metadata.name: demo-web collide when a second release shares a namespace—use {{ include "demo-web.fullname" . }} instead. Secrets committed in values.yaml expose credentials; store passwords in Kubernetes Secrets and reference them with secretKeyRef. Missing resource limits let one pod starve a node—set requests and limits in values and tune from actual usage. Database migration Jobs often need helm.sh/hook: pre-install,pre-upgrade. Skipping helm lint and helm template in CI lets broken Ingress templates fail production instead of the pipeline.

Never put database passwords or API keys in values.yaml or commit them to Git. Create Kubernetes Secret objects and reference them from templates with secretKeyRef. For enterprise clients, wire sealed-secrets, External Secrets Operator, or a cloud vault integration instead of plain values files. values.yaml should hold only non-sensitive defaults like replica count, image tags, and ingress hosts. Treat secret handling the same way you would on a production Laravel app—environment-specific credentials stay out of version control entirely.

Once the chart works locally, run helm package . to build a tarball, then helm push demo-web-0.1.0.tgz oci://registry.example.com/charts. Helm 3 supports OCI registries alongside classic chart repos. Tag chart versions the same way you tag Docker images and document required values in a README inside the chart folder. On client projects, staging installs chart version 0.3.x while production pins an exact patch after QA passes—mirroring the predictable, reversible Deployer release symlinks used on PHP sites. Pair packaging with CI pipelines that lint charts on every push.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: