Real production applications are rarely self-contained. A backend API needs a database, a cache layer, and possibly a message broker. Helm solves this with a first-class dependency system: you declare what your chart needs, Helm fetches those charts from repositories, and the entire stack is installed and upgraded as a single atomic unit. This lesson teaches you how chart dependencies work, how to share values across parent and child charts through global values, and how to compose a multi-service stack using an umbrella chart — the pattern used by platform teams at companies like Shopify and Lyft to deploy entire environments with a single helm install.
Declaring Dependencies in Chart.yaml
Dependencies are declared in the dependencies block of Chart.yaml. Each entry specifies the chart name, version constraint, and repository URL. After editing this block you must run helm dependency update — Helm resolves, downloads, and packages the dependency charts into a charts/ subdirectory as .tgz archives, and writes a Chart.lock file that pins exact resolved versions (analogous to package-lock.json).
# Chart.yaml for a backend API that depends on PostgreSQL and Redis
apiVersion: v2
name: backend-api
description: Production API service with managed dependencies
type: application
version: 1.4.0
appVersion: "2.9.1"
dependencies:
- name: postgresql
version: "15.3.x" # semver range — resolves latest 15.3.z
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled # can be disabled at install time
- name: redis
version: "19.x.x"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
- name: common
version: "2.x.x" # internal helpers library chart
repository: "https://charts.bitnami.com/bitnami"
# After editing Chart.yaml, always run:
helm dependency update ./backend-api
# Output:
# Hang tight while we grab the latest from your chart repositories...
# ...Successfully got an update from the "bitnami" chart repository
# Update Complete. Happy Helming!
# Saving 3 charts
# Downloading postgresql from repo https://charts.bitnami.com/bitnami
# Downloading redis from repo https://charts.bitnami.com/bitnami
# Downloading common from repo https://charts.bitnami.com/bitnami
# Deleting outdated charts
ls backend-api/charts/
# common-2.19.1.tgz postgresql-15.3.4.tgz redis-19.6.2.tgz
# Chart.lock is generated — commit it to git so CI reproduces exact versions
cat backend-api/Chart.lock
Always commit Chart.lock.Chart.lock pins the exact resolved version of every dependency (and transitive dependency). Without it, two engineers running helm dependency update at different times can resolve different patch versions — producing different deployments from the same commit. Treat it like go.sum or yarn.lock.
Passing Values to Subcharts
Once a dependency is downloaded, you configure it by nesting its values under a key that matches the chart name in your parent values.yaml. Helm routes those values into the subchart automatically — the subchart never sees the parent's top-level values unless you use global values (covered next).
# values.yaml for backend-api
replicaCount: 3
image:
repository: myregistry.io/backend
tag: "2.9.1"
# Subchart values: key must match the dependency name in Chart.yaml
postgresql:
enabled: true
auth:
username: api_user
password: "" # override with --set or a sealed secret
database: api_production
primary:
persistence:
size: 50Gi
storageClass: gp3
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2
memory: 4Gi
redis:
enabled: true
auth:
enabled: true
password: ""
replica:
replicaCount: 2
master:
persistence:
size: 8Gi
Inside your parent chart's own templates you reference the subchart's generated Service name to wire the connection. The conventional naming pattern for Bitnami charts is {{ include "common.names.fullname" .Subcharts.postgresql }}.{{ .Release.Namespace }}.svc.cluster.local, but in practice you use a template helper or compute the name via the release name. A safer approach is to expose the hostname as a value your app reads:
By default, a subchart is isolated: it sees only the values namespaced under its own key. Global values break this isolation intentionally — any value placed under the global: key in the parent is visible to every subchart and to the parent itself, without any namespacing. This is the canonical way to share cross-cutting configuration like image registry, environment name, or cluster region.
Global values flow down to every subchart automatically; parent-scoped values stay isolated to the parent.
# values.yaml — global block is injected into every subchart
global:
imageRegistry: myregistry.io # Bitnami charts honor this key
imagePullSecrets:
- name: regcred
storageClass: gp3
environment: production
postgresql:
enabled: true
image:
# Bitnami picks up global.imageRegistry automatically
tag: "16.3.0-debian-12-r14"
auth:
username: api_user
database: api_production
existingSecret: backend-api-db-creds # reference to an externally managed Secret
redis:
enabled: true
auth:
existingSecret: backend-api-redis-creds
existingSecretPasswordKey: redis-password
Never put plaintext passwords in values.yaml committed to git. Use existingSecret to reference a Kubernetes Secret managed outside Helm (via Sealed Secrets, External Secrets Operator, or AWS Secrets Manager CSI driver). This pattern separates secret lifecycle from chart lifecycle — a security boundary that matters enormously when charts are stored in public or semi-public repositories.
Umbrella Charts
An umbrella chart (also called a "meta chart" or "app of apps") is a chart whose sole purpose is to group and configure a set of subcharts. It has little or no content in its own templates/ directory — it exists purely to declare dependencies and provide a single values file that configures the entire stack. This is how platform teams at scale deploy entire environments: one helm install platform ./umbrella --values prod.yaml brings up API, worker, database, cache, and monitoring in one atomic operation.
# Structure of an umbrella chart
platform-umbrella/
├── Chart.yaml # declares all service subcharts as dependencies
├── Chart.lock # pinned resolved versions — ALWAYS commit this
├── values.yaml # default values for all subcharts
├── values-dev.yaml # dev environment overrides
├── values-prod.yaml # prod environment overrides
├── charts/ # downloaded .tgz subcharts (gitignored or committed)
│ ├── backend-api-1.4.0.tgz
│ ├── frontend-2.1.0.tgz
│ ├── worker-1.1.3.tgz
│ └── postgresql-15.3.4.tgz
└── templates/ # usually empty, or contains only NOTES.txt
└── NOTES.txt
# Deploy the entire platform to production in one command
helm dependency update ./platform-umbrella
helm upgrade --install platform ./platform-umbrella \
--namespace platform-prod \
--create-namespace \
--values platform-umbrella/values.yaml \
--values platform-umbrella/values-prod.yaml \
--set global.imageTag="${CI_COMMIT_SHA}" \
--atomic \
--timeout 10m \
--wait
# To disable monitoring in a lightweight dev environment:
helm upgrade --install platform ./platform-umbrella \
--values platform-umbrella/values-dev.yaml \
--set monitoring.enabled=false \
--set postgresql.primary.persistence.size=5Gi
Alias and Tags
Sometimes you need to install the same chart twice as two different subcharts — for example, two separate PostgreSQL databases for different services. Use the alias field to give each instance a unique name, which becomes the key you use in values.yaml:
Production pitfall — dependency chart cache staleness. The charts/ directory is populated by helm dependency update. If you commit the .tgz files, they can drift from Chart.lock when a teammate updates dependencies but forgets to re-commit the archives. The safest CI pattern is: never commit charts/*.tgz (add it to .helmignore), and always run helm dependency build (faster than update — uses Chart.lock without re-resolving) in your CI pipeline before helm upgrade. This guarantees every build uses exactly the pinned versions.
Putting It Together — CI Pipeline Pattern
A production CI pipeline for an umbrella chart looks like this:
Use helm dependency build in CI, not helm dependency update.build reads Chart.lock and downloads exactly those versions without hitting repository servers to re-resolve ranges. This is faster, deterministic, and prevents a surprising version bump if an upstream chart releases a new patch between your two CI runs.
With dependencies, global values, and umbrella charts mastered, you have the full compositional toolkit to manage complex production stacks. The next lesson covers hooks and tests — how to run database migrations, smoke tests, and readiness checks as integral parts of the Helm release lifecycle.