Composing large charts from small components: the dependencies field in Chart.yaml, the helm dependency commands, parent-child relationships and value inheritance, global values, subchart overrides, and advanced patterns like import-values, condition, tags, and alias.

After episode 10, where we built named templates and helpers — the abstraction that keeps one chart tidy and consistent — in this episode we go up one level of scale: composing large charts from smaller charts. The topic is dependencies and subcharts, Helm's mechanism for declaring "this chart needs that chart" and combining them into one release.
Why does this matter? Almost no production application stands alone. A web application needs a database, maybe Redis for caching, an ingress controller for traffic, and Redis Sentinel for failover. Copying all those component manifests into one giant chart produces a chart that's hard to maintain and impossible to upgrade partially. Dependencies give you the same mental model as a package manager: just as package.json in Node declares the libraries an application needs, dependencies in Chart.yaml declares supporting charts. npm install pulls all dependencies and locks them in a lockfile; helm dependency update does the same. After this episode, you'll no longer stitch charts together manually — you'll compose them. And this pattern also becomes the bridge to library charts in episode 19, where composition is everything.
Dependencies are declared in the dependencies field of Chart.yaml. Each entry describes the chart needed, where to get it from, and under what condition it's active:
apiVersion: v2
name: myapp
description: Web application with database and cache
type: application
version: 0.1.0
appVersion: "1.16.0"
dependencies:
- name: postgresql
version: "15.5.3"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "19.6.4"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
tags:
- cache
- name: local-helper
version: "0.2.0"
repository: "file://../local-helper"
tags:
- test-utilsLet's break down each important field:
name and version — the dependency chart's identity. version here isn't just a number: it's a version constraint that can use ranges like ">=1.0.0 <2.0.0", "~1.2", "^1.5.3", or "1.2.x". Helm selects the highest version matching the constraint during dependency update.repository — the chart source. It can be an HTTP(S) URL (https://charts.bitnami.com/bitnami), an OCI URL (oci://registry-1.docker.io/bitnamicharts), or a local file path (file://../local-helper) for development.condition — the values path name that determines whether the dependency gets enabled. The path value is evaluated as a boolean; if falsy, the dependency is skipped.tags — a set of tags that can enable/disable a group of dependencies at once from a single value in values.alias — an alias name so the same chart can be installed twice under different names (for example, two Redis instances: one for cache, one for queue).Important
Note the difference between condition and tags: condition controls per-dependency, tags control per-group. Both can be combined — the effective logic is OR: a dependency is enabled if its condition is true OR all the tags attached to it are true. Forgetting this OR rule is one of the most common sources of confusion among Helm users.
Once dependencies are declared, you need to pull those charts into the charts/ directory. Three commands you must memorize:
helm dependency update ./myapp
helm dependency build ./myapp
helm dependency list ./myapphelm dependency update — downloads the dependency charts according to the constraints in Chart.yaml, saves them as .tgz archives in charts/, and writes the Chart.lock file. The lockfile records the exact versions downloaded, so subsequent builds are deterministic.helm dependency build — reads Chart.lock and downloads the exact same versions without re-checking constraints. This is the equivalent of npm ci — used in CI/CD so builds are always reproducible.helm dependency list — shows a status table of dependencies: name, version, repository, and status (ok, missing, wrong version).helm dependency update ./myapp
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 2 charts
Downloading postgresql from repo https://charts.bitnami.com/bitnami
Downloading redis from repo https://charts.bitnami.com/bitnami
Deleting outdated chartsThe final result is a structure like this:
myapp/
├── Chart.lock
├── Chart.yaml
├── charts/
│ ├── postgresql-15.5.3.tgz
│ └── redis-19.6.4.tgz
├── templates/
└── values.yamlWhen a chart is packaged (episode 16), the already-downloaded subcharts get wrapped into the archive too. That's why the common team practice is committing Chart.yaml and Chart.lock, but not the charts/*.tgz files — a CI pipeline running helm dependency build puts them there. This keeps the repository small and prevents accidentally committing build binaries.
Once a dependency is downloaded into charts/, it becomes a subchart of the main chart (the parent). The relationship isn't just a folder: it's a runtime relationship that affects how values are accessed and how templates are rendered.
Some important facts about the parent-child relationship:
condition/tags), and its resources are installed as part of the parent's release.global values. It can't read a sibling chart's values.yaml.export/import-values, which we'll cover).Release.Name — so if you helm install myapp ./myapp, the postgresql subchart creates resources named myapp-postgresql. This is why two releases of the same chart in the same cluster don't clash.The analogy is a company directory: the parent is a department, subcharts are the teams inside it. Teams work with the mandate and resources given by the department, can't freely use other teams' resources, and the whole department's performance is judged as one unit in front of management (the release).
What if you want a single value seen by all subcharts at once? That's where the global section in values.yaml comes in. Values under global: are available to the parent and every subchart, no matter how deep they sit.
global:
imageRegistry: registry.internal.example.com
imagePullSecrets:
- name: regcred
storageClass: fast-ssd
postgresql:
enabled: true
image:
registry: {{ .Values.global.imageRegistry }}
persistence:
storageClass: {{ .Values.global.storageClass }}
redis:
enabled: true
image:
registry: {{ .Values.global.imageRegistry }}Notice that inside a subchart, global values are accessed via .Values.global.* — the postgresql and redis subcharts both read the registry from a single source. This pattern is very useful for cross-chart things: internal image registries, pull secrets, standard storage classes, and cluster labels/names. The benefit is real in environments that force images to be pulled from a mirror/private registry — you change one global value instead of overriding the registry in every subchart one by one.
Note
There's an important rule in value inheritance: global values can't be overridden by a subchart for other charts (a subchart can't "force" a value onto a sibling chart), but values passed down by the parent (non-global) are indeed merged into the subchart's values. If the same key appears in the parent's values and the subchart's defaults, the parent's key wins — this is the override mechanism we'll dissect next.
Override is the core of dependency configuration. The way it works is simple and elegant: in the parent's values.yaml, create a section with the same name as the dependency (or its alias). The values inside that section are merged over the subchart's default values.yaml.
postgresql:
auth:
username: myapp
database: myapp
existingSecret: myapp-db-secret
primary:
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 1Gi
persistence:
size: 20Gi
metrics:
enabled: true
redis:
architecture: standalone
master:
resources:
requests:
cpu: 100m
memory: 256MiThe key to this block: you only write the values you want to change — selective overrides. Subchart values you don't mention keep using the defaults from that subchart's values.yaml. This keeps the parent's values.yaml concise, and it's the main reason Bitnami charts ship dozens of values without requiring them all to be filled in.
If you don't want to bother reading the subchart docs to learn its values structure, use --set at install time (the highest precedence from episode 6):
helm upgrade --install myapp ./myapp \
--set postgresql.auth.password=$DB_PASSWORD \
--set redis.master.resources.requests.cpu=100mAnd to see the truly effective values (after all merging) on an existing release, helm get values is the most honest debugging tool:
helm get values myapp
helm get values myapp --revision 2Production charts often need more than simple overrides. These four patterns are the most frequently used:
By default, a subchart can't "export" values to the parent. The import-values pattern reverses this direction: a subchart can export values via an exports key in its values.yaml, and the parent imports those values.
dependencies:
- name: postgresql
version: "15.5.3"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
import-values:
- child: exports.connection
parent: databaseIf the subchart's values.yaml contains:
exports:
connection:
host: "myapp-postgresql"
port: 5432Then in the parent, you can access {{ .Values.database.host }} — values from the subchart imported into the parent scope. This pattern is often used to connect interdependent charts: the main application reads its database connection configuration from its subchart.
condition and tags were already mentioned. Combining them produces very fine-grained control. Tag values are defined in the parent's values.yaml and are global to subcharts using those tags:
tags:
cache: false
test-utils: false
redis:
enabled: trueIn the example above, a dependency that tags itself cache won't be active while tags.cache is false — even if redis.enabled is true. This is very useful for development environments: turn on "heavy" dependencies only when needed.
Want two instances of the same subchart in one release? alias is the answer:
dependencies:
- name: redis
version: "19.6.4"
repository: "https://charts.bitnami.com/bitnami"
condition: redis-cache.enabled
alias: redis-cache
- name: redis
version: "19.6.4"
repository: "https://charts.bitnami.com/bitnami"
condition: redis-queue.enabled
alias: redis-queueWith aliases, the values overrides use the alias name rather than the original name — redis-cache.master.resources, redis-queue.architecture — and the generated resources also use the alias prefix so they don't clash. This pattern is often used for cache + queue, or two databases with different roles.
Since Helm 3.8, repository can also be an OCI registry:
dependencies:
- name: redis
version: "19.6.4"
repository: "oci://registry-1.docker.io/bitnamicharts"OCI's advantages will be fully covered in episode 18, but for dependency context, the key things to know: OCI dependencies are downloaded from the same registry as container images, use the same authentication, and suit organizations already built around a container registry.
To close the discussion, here are the habits that save teams from dependency problems:
Chart.lock. It's the source of truth for exact versions; don't let each developer pull a different version because of loose constraints.helm dependency build in CI/CD, not update. build is deterministic; update can pull new versions if constraints are loose and the lockfile is out of sync.">=1.0.0 <2.0.0" limits safe minor upgrades while avoiding major upgrades that could bring breaking changes.values-dev.yaml, values-staging.yaml, values-prod.yaml each override enabled and dependency resource sizing as needed.helm dependency update ./myapp && git add Chart.lock
helm dependency build ./myapp
helm lint ./myapp && helm template myapp ./myapp --debugIn episode 11 you've mastered chart composition: declaring dependencies in Chart.yaml complete with version constraints, HTTP/OCI/file repositories, and condition and tags; running helm dependency update, build, and list along with Chart.lock's role; understanding the parent-child relationship and value inheritance rules; sharing configuration across charts via the global section; selectively overriding subchart values; and using the advanced import-values, tags, alias, and OCI registry patterns. Most importantly, you can now view charts as something to be composed, not written whole from scratch — a shift in thinking that will determine the scale of charts you can manage.
In the next episode, episode 12, we cover what makes Helm more than just templating: hooks and lifecycle management — how to run Jobs at key points in a release's lifecycle (install, upgrade, rollback, delete), including database migrations before an upgrade and smoke tests after an install. Make sure your understanding of the parent-child relationship is fresh, because hooks work in the same release context we just built. See you in episode 12!