A chart can depend on other charts. That gives you the packaging benefit — “install Postgres” is a line of YAML — and a coupling cost that is easy to miss: everything under one umbrella becomes one release, with one revision history and one failure.
Topic 1: Declaring Dependencies
# Chart.yaml
apiVersion: v2
name: platform
version: 1.2.0
dependencies:
- name: postgresql
version: "15.5.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: "19.x.x"
repository: oci://registry-1.docker.io/bitnamicharts
condition: redis.enabled
tags: [cache]
- name: api
version: "0.4.x"
repository: "file://../api" # a local path, for a monorepo
- name: common
version: "1.2.0"
repository: oci://ghcr.io/acme/charts # a library chart
helm dependency update . # resolve, download into charts/, write Chart.lock
helm dependency build . # install exactly what Chart.lock says — use this in CI
helm dependency list .
update versus build matters in a pipeline: update re-resolves version ranges and may pull a newer patch release; build installs exactly what the lock file pins. CI should use build, so a dependency does not change under you between the commit and the deploy.
Commit Chart.lock. Do not commit charts/. The lock file pins resolved versions and is what makes builds reproducible; the charts/ directory is downloaded output and belongs in .gitignore. (The exception is an air-gapped environment where vendoring is the point — then commit both and say so in the README.)
Version ranges follow semver: "15.5.x" allows patch updates, "~15.5.0" is equivalent, "^15.0.0" allows minor updates, "15.5.2" pins exactly. For anything stateful, pin tightly — a minor version of a database chart can change a StatefulSet’s storage layout.
Topic 2: Overriding a Subchart’s Values
Subchart values live under the subchart’s name in the parent’s values:
# platform/values.yaml
postgresql: # the subchart NAME (or its alias)
auth:
database: checkout
username: checkout
primary:
persistence:
size: 20Gi
resources:
requests: { cpu: 500m, memory: 1Gi }
redis:
architecture: standalone
auth:
enabled: true
api:
replicaCount: 3
image:
tag: "1.6.0"
Two rules that are not obvious:
- A subchart cannot read the parent’s values. It sees its own values plus
global. Any coupling must be explicit. - The parent always wins on keys it sets, because the parent’s values have higher precedence than the subchart’s own defaults.
global is the shared channel, and it is the only one:
global:
imageRegistry: ghcr.io/acme
imagePullSecrets: [regcred]
storageClass: fast-ssd
Every subchart sees .Values.global.* — but only if the subchart’s templates actually read it. Most well-known charts support global.imageRegistry and global.storageClass; a chart that does not read them ignores yours silently. Check helm show values before assuming.
Keep global small. It is a namespace shared by every chart in the tree, including third-party ones, and a collision produces behaviour nobody intended.
Topic 3: Conditions, Tags and Aliases
condition points at a boolean in values and enables or disables the dependency:
dependencies:
- name: postgresql
condition: postgresql.enabled # first existing path wins
postgresql:
enabled: false # use a managed database in prod instead
This is the standard pattern for “bundled dependency for dev, external service in production”, and it is why most public charts ship with an embedded database that you turn off.
tags enable groups of dependencies at once:
dependencies:
- name: redis
tags: [cache]
- name: memcached
tags: [cache]
tags:
cache: false # both off
condition beats tags when both are present.
alias installs the same chart more than once:
dependencies:
- name: postgresql
version: "15.5.x"
repository: https://charts.bitnami.com/bitnami
alias: postgresql-orders
- name: postgresql
version: "15.5.x"
repository: https://charts.bitnami.com/bitnami
alias: postgresql-billing
Values then go under the alias, not the chart name — postgresql-orders: and postgresql-billing:. This is how one umbrella runs two databases without forking anything.
import-values pulls values up from a child into the parent’s namespace, which is occasionally the cleanest way to let a chart expose computed configuration to its siblings. It is rarely needed; reach for global first.
Topic 4: The Coupling Cost
An umbrella chart makes everything one release. That has consequences worth deciding on deliberately rather than discovering:
- One revision history. Upgrading the API bumps the whole release, including revisions for subcharts nothing changed in.
- One failure domain. If the Postgres subchart’s upgrade fails, the whole
helm upgradefails — and with--atomic, your API rolls back too. - One rollback. You cannot roll back only the API.
- One set of hooks, ordered by weight across every chart in the tree.
- Rendering order is by kind, not by chart. Helm sorts all resources from all subcharts into its install order; a subchart does not “complete” before the next begins. If the API needs the database ready, that is an init container or a probe — not an ordering assumption.
When an umbrella is the right shape: components that are genuinely deployed and versioned together, and a dev-environment convenience stack. When separate releases are better: anything with an independent release cadence, anything stateful you would rather not couple to an application rollback, and anything a different team owns.
A middle path many teams settle on: separate releases per component, orchestrated by GitOps (Argo CD Applications or Flux HelmReleases), which gives you per-component rollback with declarative ordering — covered in the delivery lesson.
Topic 5: Library Charts
A library chart (type: library) contains only named templates, has no templates of its own, and cannot be installed. It is how an organisation shares helper logic without copying _helpers.tpl between repositories.
# common/Chart.yaml
apiVersion: v2
name: common
type: library
version: 1.2.0
{{/* common/templates/_labels.tpl */}}
{{- define "common.labels" -}}
app.kubernetes.io/name: {{ include "common.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" }}
{{- end }}
Consumers add it as a dependency and include its templates. One place to fix the label convention or the security-context defaults across every chart you own.
The trade-off is version coordination: changing a library template requires a version bump and a helm dependency update in each consumer. Worth it at around five charts; overhead at two.
Topic 6: Auditing What You Depend On
A chart from a public repository runs in your cluster with whatever permissions its manifests request. Read it before installing:
helm show chart bitnami/postgresql
helm show values bitnami/postgresql | head -80
helm pull bitnami/postgresql --version 15.5.0 --untar # read the templates
helm template test ./postgresql | grep -E 'kind:|image:|serviceAccountName|privileged'
Three checks that take two minutes and are worth doing for anything going into production:
- What images does it pull, and from where? A chart pointing at an unfamiliar registry is a supply-chain decision.
- What RBAC does it create?
grep -A5 'kind: ClusterRole'in the rendered output — a chart requesting cluster-wide privileges should have a reason. - Is it maintained? Check the repository’s last release. Abandoned charts pin abandoned images.
Pin dependencies to exact versions in production, and update deliberately with a diff:
helm diff upgrade platform . -f values/prod.yaml
Try it yourself: add a dependency with a condition, install once with it enabled and once disabled, and diff the two rendered outputs. Seeing exactly which resources appear and disappear makes the mechanism concrete — and it is how you verify a chart’s enabled flag actually does what it claims.
Common mistake: committing the charts/ directory along with Chart.lock, then editing a vendored subchart directly to fix something. The next helm dependency update overwrites the edit without warning, the fix vanishes, and nobody remembers it was there. Fork the chart properly, or override through values — never edit vendored output.