Skip to main content
Helm Chart Anti-Patterns We've Stopped Using

Helm Chart Anti-Patterns We've Stopped Using

KU
· Founder & Chief Editor
• 8 min read•Updated •77 views

Helm gives you a lot of rope. The patterns we used that backfired, the ones we replaced them with, and what to skip if you're starting today.

Key takeaways

  • The Helm chart anti-patterns that hurt most are templating values that never vary, deeply nested values, subcharts for your own services, hooks for every-deploy work, floating dependency versions, secrets in values files, and infinitely flexible charts.
  • Template only what changes, keep values flat, pin dependencies through Chart.lock, and keep secrets in an external store.

The Helm anti-patterns we've dropped: templating values that never change, deeply nested values, subcharts for our own services, hooks for every-deploy work, floating dependency versions, secrets in values files, and charts flexible enough to emit any Deployment. The replacement is boring: template only what varies, flat values, locked dependencies, external secrets, opinionated charts.

Verified against

Helm 4.3.1 (released 2026-10-15), against the helm.sh chart best practices (values and dependencies) and chart hooks pages. Checked October 2026. Some best-practice pages still carry a "not yet updated for Helm 4" banner.

We run a mix of first-party charts (we wrote them) and third-party charts (upstream charts with values overrides). This is the list of patterns we used and abandoned, plus what we use instead.

What Helm is good at

Helm earns its place for distributing reusable, parameterized Kubernetes manifests with a release lifecycle (upgrade, rollback) on top. For shipping software into other teams' clusters, it is the standard. For your own services in your own cluster, it is one option among several.

We use Helm for the cases where third-party software comes packaged as a chart (e.g. cert-manager, kube-prometheus-stack, sealed-secrets) and for our internal "platform" charts that get reused across many namespaces. For per-service application manifests, we use plain YAML in Argo CD; Helm vs Kustomize covers that split in detail.

Anti-pattern 1: Templating everything

The most common mistake. Replicas vary per environment, so {{ .Values.replicas }}. Then the image tag, then resource requests, ConfigMap names, labels, annotations, environment variables...

A year later your values.yaml has hundreds of lines and your templates are mostly Go template syntax. To make a change you have to chase values through several layers of indirection.

What we do instead: only template what actually varies. Static YAML for the parts that don't change. Our typical chart's values.yaml fits on one screen.

Things that almost never need templating: apiVersion, most labels, ConfigMap key structure, and Service ports. If you're templating a value that's the same in every environment you've ever deployed, just hardcode it. The flexibility you're "preserving" is imaginary.

Anti-pattern 2: Deeply nested values.yaml

YAML
# values.yaml (hard to read)
service:
  api:
    deployment:
      replicas:
        production: 5
        staging: 2
      resources:
        limits:
          cpu: "1"
          memory: 1Gi

In the template: {{ .Values.service.api.deployment.replicas.production }}. Long. Brittle. Get the path wrong and Helm silently renders empty. Helm's own values guide agrees: "In most cases, flat should be favored over nested," because every layer of nesting needs an existence check.

What we do instead: flatter values, more explicit values files per environment. Instead of one nested values.yaml, have values-production.yaml and values-staging.yaml with flat structures:

values-production.yaml
replicas: 5
cpuLimit: "1"
memoryLimit: 1Gi

Use camelCase keys, as the same guide recommends; hyphenated keys break Go template dot notation and force you into index.

Templates are shorter; mistakes are obvious; per-env differences are visible at a glance.

Anti-pattern 3: Subcharts for everything

The temptation: every service is a subchart, so helm install my-platform gets the whole stack. What goes wrong: overriding a subchart's value requires parent.subchart.thing, multi-level dependencies create version coupling, and a bug in one subchart blocks the parent release.

What we do instead: one chart per service, deployed independently. If they need to share data (a shared ConfigMap, a service URL), they read it from Kubernetes directly. Each chart's release is independent; no parent-child coupling.

The exception is distribution: a chart that ships an app plus its required Redis and Postgres can reasonably use subcharts.

Anti-pattern 4: helm.sh/hook for everything

Helm hooks run jobs at specific lifecycle phases (pre-install, post-upgrade, etc.). Useful for database migrations and one-shot setup tasks.

What goes wrong: people use hooks for things that don't fit the model. "Run this Job whenever we deploy" becomes a post-upgrade hook, and then a rollback runs pre-rollback/post-rollback hooks instead, so the Job doesn't run. Hook resources aren't managed as part of the release, so helm uninstall leaves them behind. And a failing hook Job fails the entire release, which turns a flaky seeding script into a blocked deploy, or, if the process dies mid-hook, a release stuck in pending-upgrade.

What we do instead:

  • Database migrations: yes, hooks. That's what they're for.
  • One-time data seeding: hooks, with an explicit helm.sh/hook-delete-policy. before-hook-creation is the default; add hook-succeeded if you don't want finished Jobs lingering.
  • Anything that should run on every deploy: not a hook. Use an init container in the Deployment or a separate Argo CD pre-sync resource.

The hook semantics are confusing. Use them deliberately, not as a catch-all.

Anti-pattern 5: Hand-maintained _helpers.tpl

Every Helm chart has a templates/_helpers.tpl file with snippets like:

YAML
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}

They accumulate. Charts end up with dozens of helpers, half of them used once.

What we do instead: start from helm create output and ruthlessly prune. helm create gives you a starter chart with the standard helpers. We keep the ones we actually use, delete the rest. Most charts end up with 5-10 helpers.

Anti-pattern 6: Pinning to latest or floating versions

In Chart.yaml:

YAML
dependencies:
  - name: redis
    version: ">=14.0.0"

Or worse, version: "*". Result: deploys behave differently on different days because the dependency resolved to different versions.

Helm's dependency guide actually recommends ranges, with patch-level ~1.2.3 (meaning >= 1.2.3, < 1.3.0) as the suggested default. That's sensible for charts you publish to other people. For charts we deploy ourselves, we go further: exact pins plus a committed Chart.lock. When we want to upgrade, we change Chart.yaml in a PR and regenerate the lock:

Terminal
helm dependency update ./charts/api
git diff charts/api/Chart.lock
helm dependency build ./charts/api

build installs exactly what the lock file says, so every deploy of a given chart revision resolves to identical dependencies.

For Helm itself, we pin to a specific minor version too (in our Argo CD config) for the same reason.

Anti-pattern 7: Putting secrets in values.yaml

The hard rule: secrets never live in values files. Sounds obvious; teams still do it.

What we do instead: External Secrets Operator + AWS Secrets Manager. The chart includes an ExternalSecret resource that references the secret name in AWS; the operator syncs it into a Kubernetes Secret at deploy time. Actual secret material never touches the values files or the Git repo.

For some legacy charts that haven't been migrated, we use sealed-secrets: secrets are encrypted in Git with a key only the cluster has. Less ideal than ESO (sealed-secrets requires re-encrypting on every change) but better than plaintext.

Anti-pattern 8: Unbounded values flexibility

The chart accepts extraEnv, extraVolumes, extraVolumeMounts, podAnnotations, extraLabels, extraInitContainers, extraSidecars, ad infinitum. Pretty soon the chart can produce any Deployment shape; the chart adds zero value over plain YAML at that point.

What we do instead: opinionated charts that do one thing well. If a use case needs features the chart doesn't support, use a different chart or plain YAML. The chart's value is its opinionatedness; "infinitely flexible" charts give you the worst of both worlds.

What we use instead of Helm for most internal services

For our own services we use plain YAML in Argo CD repos with Kustomize overlays for per-environment differences, and Argo CD ApplicationSets to generate per-namespace or per-cluster deployments from one template. That works because we deploy to our own clusters, Argo CD already handles sync and rollback, and "small differences per environment" is exactly what overlays model. We still use Helm for bundled third-party software, internal platform charts other teams install, and historical charts we haven't migrated.

Questions people ask

Should Helm chart values be flat or nested?

Flat, in most cases. Helm's values best-practices page says "flat should be favored over nested," because every nesting layer forces an existence check in templates and a wrong path silently renders empty. Nest only where a group of related settings has at least one required key, such as image.repository and image.tag. Use camelCase keys, never hyphens.

Should I pin exact versions for Helm chart dependencies?

Helm's docs suggest patch-level ranges like ~1.2.3 for charts you distribute. For charts you deploy yourself, reproducibility matters more: commit Chart.lock, run helm dependency build in CI so it installs exactly the locked versions, and change versions only through a reviewed helm dependency update. Never use open ranges like >=14.0.0 in a deployed chart.

When should I use Helm hooks?

For lifecycle tasks that genuinely belong to install or upgrade: database migrations and one-time setup. Hook resources aren't tracked with the release, so helm uninstall won't remove them, and a failed hook Job fails the whole release. Anything that must run on every deploy, including rollbacks, belongs in an init container or an Argo CD sync hook instead.

The call we'd make

Template only what varies, keep values flat with per-environment files, pin dependencies through a committed Chart.lock, keep secrets out of values entirely, and deploy one chart per service. Use Helm for shipping software to other people's clusters and for third-party charts; reach for Kustomize for your own app manifests. The anti-patterns above are what happens when teams stretch Helm over problems it doesn't solve cleanly.

Sources

React
Share this post
Cite this article
Plain text

Kiril Urbonas (2026). Helm Chart Anti-Patterns We've Stopped Using. DevOpsNess. Updated 2026-10-31. https://www.devopsness.com/blog/helm-chart-anti-patterns

Markdown

[Helm Chart Anti-Patterns We've Stopped Using](https://www.devopsness.com/blog/helm-chart-anti-patterns) (Kiril Urbonas, DevOpsNess, updated 2026-10-31)

KU

Kiril Urbonas

Founder & Chief Editor

590 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.