Learn Helm Chart - Managing Dependencies & Subcharts
Episode 11 of 30

Learn Helm Chart - Managing Dependencies & Subcharts

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.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

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.

The Dependencies Concept in Chart.yaml

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:

Chart.yaml with dependencies
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-utils

Let'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.

Dependency Commands: update, build, list

Once dependencies are declared, you need to pull those charts into the charts/ directory. Three commands you must memorize:

Managing chart dependencies
helm dependency update ./myapp
helm dependency build ./myapp
helm dependency list ./myapp
  • helm 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 output
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 charts

The final result is a structure like this:

Chart structure with subcharts
myapp/
├── Chart.lock
├── Chart.yaml
├── charts/
│   ├── postgresql-15.5.3.tgz
│   └── redis-19.6.4.tgz
├── templates/
└── values.yaml

When 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.

Subcharts: The Parent-Child Relationship

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:

  1. The parent controls existence. A subchart is rendered only if its condition is met (from condition/tags), and its resources are installed as part of the parent's release.
  2. A subchart never knows other charts. A subchart only sees itself, its parent, and global values. It can't read a sibling chart's values.yaml.
  3. Values flow down, not up. The parent can pass values to a subchart; a subchart never returns values to the parent (except via export/import-values, which we'll cover).
  4. Scope inside a subchart. Subchart resource names are computed from the parent's 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).

Global Values: Sharing Values with All Subcharts

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.

values.yaml - global values
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.

Overriding Subchart Values

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.

values.yaml - overriding subchart values
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: 256Mi

The 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):

Override a subchart from the CLI
helm upgrade --install myapp ./myapp \
  --set postgresql.auth.password=$DB_PASSWORD \
  --set redis.master.resources.requests.cpu=100m

And to see the truly effective values (after all merging) on an existing release, helm get values is the most honest debugging tool:

View a release's effective values
helm get values myapp
helm get values myapp --revision 2

Advanced Dependency Patterns

Production charts often need more than simple overrides. These four patterns are the most frequently used:

import-values

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.

Chart.yaml - import-values from a subchart
dependencies:
  - name: postgresql
    version: "15.5.3"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
    import-values:
      - child: exports.connection
        parent: database

If the subchart's values.yaml contains:

Subchart values.yaml - exporting values
exports:
  connection:
    host: "myapp-postgresql"
    port: 5432

Then 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 for Group Control

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:

values.yaml - controlling a group via tags
tags:
  cache: false
  test-utils: false
 
redis:
  enabled: true

In 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.

alias for Multiple Instances

Want two instances of the same subchart in one release? alias is the answer:

Chart.yaml - two Redis instances with aliases
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-queue

With 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.

OCI Repository

Since Helm 3.8, repository can also be an OCI registry:

Chart.yaml - dependency from 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.

Dependency Best Practices

To close the discussion, here are the habits that save teams from dependency problems:

  1. Commit Chart.lock. It's the source of truth for exact versions; don't let each developer pull a different version because of loose constraints.
  2. Use 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.
  3. Pin versions with reasonable constraints. ">=1.0.0 <2.0.0" limits safe minor upgrades while avoiding major upgrades that could bring breaking changes.
  4. Don't override values you don't understand. Subchart charts have many hidden values; excessive overrides make debugging impossible. Override only what's genuinely needed, and document the reason in a comment.
  5. Separate environments via values files (from episode 6): values-dev.yaml, values-staging.yaml, values-prod.yaml each override enabled and dependency resource sizing as needed.
  6. Watch the chart size. Each subchart adds to the archive size and install time. If a dependency is only used for a small part of its functionality, consider building your own leaner chart.
A clean dependency workflow
helm dependency update ./myapp && git add Chart.lock
helm dependency build ./myapp
helm lint ./myapp && helm template myapp ./myapp --debug

Conclusion

In 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!

Learn Helm Chart - Managing Dependencies & Subcharts | Learn Helm Chart