How to Use Helm for Package Management in Kubernetes

How to Use Helm for Package Management in Kubernetes

Somewhere around my fifteenth YAML file for a single application — Deployment, Service, ConfigMap, Secret, Ingress, HPA — I realized I needed a better way to manage all of it. That’s what led me to Helm, which I now think of as the closest thing Kubernetes has to a proper package manager. In this guide, I’ll cover what Helm actually is, how charts are structured, and walk through installing, customizing, and even authoring your own chart.

What Is Helm and Why Use It?

Helm packages a set of Kubernetes manifests into a single unit called a chart. Instead of applying a dozen YAML files by hand, you run one command to install, upgrade, or roll back an entire application, complete with templated values so the same chart can be reused across dev, staging, and production with different configuration.

The core concepts:

  • Chart — a package of templated Kubernetes manifests plus metadata.
  • Release — a specific instance of a chart deployed into a cluster.
  • Values — the configuration passed into a chart’s templates, usually via a values.yaml file.
  • Repository — a collection of charts you can install from, similar to an apt or npm repo.

Step 1: Install Helm

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

Output:

version.BuildInfo{Version:"v3.15.0", GitCommit:"...", GoVersion:"go1.22.1"}

Step 2: Add a Chart Repository and Install Something Real

Let’s install Prometheus using its official chart, a common real-world task:

helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm search repo prometheus-community
NAME                                    CHART VERSION   APP VERSION
prometheus-community/prometheus         25.24.0         2.53.0
prometheus-community/kube-prometheus... 61.3.2          0.75.0

Install it into its own namespace:

helm install prometheus prometheus-community/prometheus \
  --namespace monitoring \
  --create-namespace

Check the release:

helm list -n monitoring
NAME         NAMESPACE   REVISION   STATUS     CHART              APP VERSION
prometheus   monitoring  1          deployed   prometheus-25.24.0 2.53.0

Step 3: Customize Values Instead of Editing Templates Directly

Every chart ships a values.yaml with sensible defaults. Rather than hand-editing rendered manifests, you override values:

helm show values prometheus-community/prometheus > my-values.yaml

Edit the parts you care about, for example bumping retention and storage size:

# my-values.yaml (partial)
server:
  retention: "30d"
  persistentVolume:
    size: 50Gi
alertmanager:
  enabled: true

Apply with the override:

helm upgrade prometheus prometheus-community/prometheus \
  -n monitoring \
  -f my-values.yaml

You can also override individual values inline without a file:

helm upgrade prometheus prometheus-community/prometheus \
  -n monitoring \
  --set server.retention=30d \
  --set server.persistentVolume.size=50Gi

Step 4: Rolling Back a Release

If an upgrade introduces a problem, Helm keeps revision history so rollback is trivial:

helm history prometheus -n monitoring
REVISION   UPDATED                   STATUS       CHART              DESCRIPTION
1          Mon Jul 28 10:02:11 2026  superseded   prometheus-25.24.0  Install complete
2          Mon Jul 28 11:15:44 2026  deployed     prometheus-25.24.0  Upgrade complete
helm rollback prometheus 1 -n monitoring

This restores the exact manifests from revision 1 — no manual YAML archaeology required.

Step 5: Writing Your Own Chart

For your own applications, scaffold a new chart:

helm create myapp

This generates a standard structure:

myapp/
  Chart.yaml
  values.yaml
  templates/
    deployment.yaml
    service.yaml
    ingress.yaml
    _helpers.tpl
    tests/
  charts/

Chart.yaml holds metadata:

apiVersion: v2
name: myapp
description: A Helm chart for myapp
type: application
version: 0.1.0
appVersion: "1.0.0"

A trimmed templates/deployment.yaml using Go templating to pull from values.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Release.Name }}-myapp
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app: {{ .Release.Name }}-myapp
  template:
    metadata:
      labels:
        app: {{ .Release.Name }}-myapp
    spec:
      containers:
        - name: myapp
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}

And the corresponding values.yaml:

replicaCount: 2
image:
  repository: myrepo/myapp
  tag: "1.0.0"
service:
  port: 8080
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

Render it locally to check the output before installing (extremely useful for catching template mistakes):

helm template myapp ./myapp

Lint the chart for common mistakes:

helm lint ./myapp

Then install:

helm install myapp-dev ./myapp --namespace dev --create-namespace

Step 6: Managing Secrets Safely

Never put real secrets directly in values.yaml committed to git. Instead, either pass them at install time:

helm install myapp ./myapp --set secrets.dbPassword=$DB_PASSWORD

Or better, integrate with a secrets manager like Sealed Secrets, External Secrets Operator, or Vault, and reference the resulting Kubernetes Secret from your templates rather than baking credentials into values files at all.

Step 7: Helm in CI/CD

A typical GitOps-friendly upgrade step in a pipeline:

helm upgrade --install myapp ./charts/myapp \
  --namespace production \
  --set image.tag=$CI_COMMIT_SHA \
  --wait \
  --timeout 5m \
  --atomic

The --atomic flag is important — it automatically rolls back if the upgrade fails, and --wait blocks until Pods are actually ready, giving your pipeline a reliable pass/fail signal instead of a false positive.

Debugging Helm Issues

helm get manifest prometheus -n monitoring   # see exactly what was applied
helm get values prometheus -n monitoring     # see the values actually used
helm status prometheus -n monitoring         # current release status

If a release gets stuck in a pending-upgrade state after a failed deploy:

helm rollback prometheus 0 -n monitoring

Best Practices

  • Pin chart versions explicitly in production (--version 25.24.0) rather than always pulling latest.
  • Keep environment-specific values in separate files (values-dev.yaml, values-prod.yaml) layered with -f.
  • Use helm diff (a plugin) before upgrading production to preview exactly what will change.
  • Store charts for your own apps in a proper OCI registry or chart repo, versioned alongside your app releases.
  • Avoid excessive templating logic in charts — if a template needs heavy conditionals for every edge case, it may be a sign to split into multiple charts.

Common Mistakes

  • Forgetting --create-namespace and having installs silently fail against a nonexistent namespace.
  • Not linting charts before install, missing indentation errors in templates.
  • Treating values.yaml defaults as production-ready without review — many community charts default to minimal resource requests unsuitable for real workloads.

Chart Dependencies and Subcharts

Real applications often need more than one chart working together — your app plus a Redis cache plus a Postgres instance, for example. Helm supports this through dependencies declared in Chart.yaml:

apiVersion: v2
name: myapp
version: 0.1.0
dependencies:
  - name: redis
    version: "19.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
helm dependency update ./myapp

This pulls the dependent chart into myapp/charts/, and its values become configurable as a nested key in your own values.yaml:

redis:
  enabled: true
  auth:
    password: "changeme"

This pattern lets you compose complex, multi-service applications from a mix of your own charts and well-maintained community charts, without duplicating their internal logic.

Helm Hooks for Lifecycle Management

Sometimes you need something to run at a specific point in a release’s lifecycle — a database migration before the main Deployment updates, for instance. Helm hooks handle exactly this:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Release.Name }}-db-migrate
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["./migrate.sh"]
      restartPolicy: Never

The pre-upgrade,pre-install annotation ensures this Job runs and completes successfully before Helm proceeds with updating the main application resources — giving you a reliable way to sequence schema migrations ahead of a code deploy within a single helm upgrade invocation.

Testing Charts Before They Reach Production

Beyond helm lint and helm template, Helm supports test hooks that validate a release actually works after install:

apiVersion: v1
kind: Pod
metadata:
  name: {{ .Release.Name }}-test-connection
  annotations:
    "helm.sh/hook": test
spec:
  containers:
    - name: test
      image: curlimages/curl
      command: ["curl", "-f", "http://{{ .Release.Name }}-myapp:8080/healthz"]
  restartPolicy: Never
helm test myapp-dev -n dev

This spins up the test Pod, checks its exit code, and reports pass/fail — a nice automated smoke test you can wire directly into a CI/CD pipeline right after helm upgrade --install.

Working with Private Chart Repositories

For internal charts, storing them in a plain HTTP chart repo or, more commonly today, an OCI-compliant registry (many teams reuse their existing container registry) keeps versioning and distribution consistent with the rest of your artifacts:

helm package ./myapp
helm push myapp-0.1.0.tgz oci://myregistry.example.com/helm-charts
helm install myapp oci://myregistry.example.com/helm-charts/myapp --version 0.1.0

This OCI-based flow has become the more common pattern in recent Helm versions, since it avoids standing up and maintaining a separate chart-repository index.

Common Helm Anti-Patterns to Avoid

  • Templating so much conditional logic into one chart that it becomes unreadable — if you find yourself writing deeply nested {{- if }} blocks to support wildly different deployment shapes, consider splitting into separate charts instead.
  • Committing rendered Secrets (even base64-encoded) into a values file tracked in git — base64 is encoding, not encryption, and anyone with repo access can trivially decode it.
  • Relying on latest as a chart version in CI/CD, which makes deployments non-reproducible and rollbacks unreliable.
  • Skipping helm lint and helm template in CI, catching template errors only at actual deploy time in a real environment.

Summary

Helm turns the sprawl of raw Kubernetes YAML into versioned, reusable, configurable packages. Whether you’re installing complex third-party software like Prometheus or packaging your own microservices, the workflow is the same: install or upgrade a chart, override values for your environment, and roll back instantly if something goes wrong. Once you’re comfortable with charts, templates, and values, Helm becomes the backbone of a much more maintainable Kubernetes deployment process.

References

Total
2
Shares

Leave a Reply

Previous Post
How to Implement Network Policies in Kubernetes

How to Implement Network Policies in Kubernetes

Next Post
How to Set Up Kubernetes Ingress Controller

How to Set Up Kubernetes Ingress Controller

Related Posts