Back to Blog
Kubernetes
Helm
DevOps

Helm Charts in Depth: Packaging and Deploying Kubernetes Apps

OpsQuiz TeamAugust 2, 20263 min read43 views

A single application deployed to Kubernetes usually needs more than one YAML file, a Deployment, a Service, maybe a ConfigMap, an Ingress, a couple of Secrets. Multiply that across dev, staging, and production, each needing slightly different values (replica counts, resource limits, hostnames), and you end up either duplicating YAML everywhere or hand-editing the same files repeatedly. Helm exists to solve exactly this problem.

What a chart actually is

A Helm chart is a directory with a specific structure:

mychart/
  Chart.yaml          # metadata: name, version, description
  values.yaml          # default configuration values
  templates/           # Kubernetes manifest templates
    deployment.yaml
    service.yaml
    ingress.yaml
  charts/               # optional: dependency subcharts

The templates directory holds your Kubernetes manifests, but written using Go's templating syntax instead of plain YAML, so values can be substituted in at install time rather than hardcoded.

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-web
spec:
  replicas: {{ .Values.replicaCount }}
  template:
    spec:
      containers:
        - name: web
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          resources:
            requests:
              memory: {{ .Values.resources.requests.memory }}
# values.yaml
replicaCount: 2
image:
  repository: myapp
  tag: "1.4.0"
resources:
  requests:
    memory: "256Mi"

Running helm install myapp ./mychart renders every template by substituting in values, then applies the resulting plain Kubernetes YAML to the cluster. Nothing magical happens beyond that: Helm is a templating and packaging layer on top of the exact same manifests kubectl would apply directly.

Overriding values per environment

The entire point of separating values from templates is that you can override them without touching the templates at all:

helm install myapp ./mychart -f values-production.yaml
helm install myapp ./mychart --set replicaCount=5 --set image.tag=1.5.0

A common pattern is a base values.yaml with sane defaults, plus a small values-production.yaml that only overrides what's actually different for that environment (higher replica count, a real domain in the Ingress, tighter resource limits). This keeps environment-specific configuration small and explicit, instead of maintaining three near-duplicate full copies of every manifest.

Releases, and why upgrades are safer than raw kubectl apply

Every helm install creates a release, a named, tracked deployment of a chart with a specific set of values. Helm keeps a revision history for each release, which is what makes helm upgrade and helm rollback possible:

helm upgrade myapp ./mychart --set image.tag=1.6.0   # deploy a new version
helm history myapp                                    # see every past revision
helm rollback myapp 3                                 # revert to revision 3 exactly

Plain kubectl apply has no concept of a release or revision history built in, you're relying on whatever's in your Git history or CI logs to know what was deployed when. Helm tracks this natively, which is a big part of why teams reach for it once they have more than one or two services to manage.

Dependencies: charts that need other charts

A chart can declare dependencies on other charts in its Chart.yaml:

dependencies:
  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com/bitnami"

Running helm dependency update pulls those dependency charts into your chart's charts/ directory, so installing your application chart also installs and configures Redis alongside it, using values you can override the same way as any other chart value. This is how most publicly available "install this whole application stack" charts are structured internally.

Hooks: running things at specific points in the lifecycle

Sometimes you need something to happen before or after an install or upgrade, running a database migration before the new application version starts, for example. Helm hooks let you annotate a Job or Pod to run at a specific lifecycle point:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    "helm.sh/hook": pre-upgrade
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: myapp-migrate:latest
      restartPolicy: Never

pre-install, post-install, pre-upgrade, and post-upgrade are the ones you'll use most. Helm runs the hook resource, waits for it to complete, then proceeds with the rest of the release, or stops and reports a failure if the hook itself fails.

Debugging: render before you install

Because templates involve real logic (conditionals, loops, value substitution), it's easy to write one that renders into invalid YAML without realizing it until helm install fails partway through. Two commands catch this before anything touches the cluster:

helm template ./mychart -f values-production.yaml   # render everything locally, no cluster contact
helm install myapp ./mychart --dry-run --debug       # simulate an install, show what would apply

Getting into the habit of running helm template before every real install, especially after editing a template file, catches most templating mistakes immediately rather than mid-deployment.

Helm doesn't remove the need to understand the underlying Kubernetes objects, it just stops you from copy-pasting the same YAML with tiny edits across every environment and every service. If you want to solidify the Kubernetes fundamentals a chart is ultimately templating, the Kubernetes quiz on OpsQuiz is a good next step.

    Welcome to OpsQuiz!

    Real scenario-based DevOps questions, hands-on practice, and clear explanations for every answer.