Theory
Composing complex applications from reusable Helm chart building blocks
Why Dependencies Matter
Most real-world applications do not run in isolation. A typical web application needs a database, a cache layer, and possibly a message broker. Rather than bundling all of those Kubernetes manifests into a single monolithic chart, Helm lets you declare dependencies -- external charts that are pulled in automatically and rendered alongside your own templates.
This dependency model gives you several advantages: you can reuse battle-tested community charts (Bitnami PostgreSQL, Redis, etc.) without reinventing their templates, you can version-lock your infrastructure components independently, and you can toggle pieces on or off depending on the target environment.
Declaring Dependencies in Chart.yaml
Dependencies are declared in the dependencies block of your Chart.yaml. Each entry requires three fields: name, version, and repository.
apiVersion: v2
name: myapp
version: 1.0.0
appVersion: "2.5.0"
dependencies:
- name: postgresql
version: "13.2.24"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
tags:
- database
- name: redis
version: "18.6.1"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
tags:
- cache
The name must match the chart name in the remote repository. The version field accepts exact versions or semantic version ranges. The repository is the URL of the Helm repository (or an OCI registry with the oci:// prefix).
helm dependency update vs. helm dependency build
After declaring dependencies you need to fetch them. Helm provides two commands for this:
helm dependency update ./myapp-- ReadsChart.yaml, resolves the best matching version for each dependency, downloads the.tgzarchives into thecharts/directory, and writes or updatesChart.lock.helm dependency build ./myapp-- Reads the existingChart.lockfile and downloads the exact versions recorded there. This guarantees reproducible builds across machines and CI pipelines.
Think of Chart.lock like package-lock.json in Node.js or poetry.lock in Python. Commit Chart.lock to version control and add charts/*.tgz to .gitignore.
The charts/ directory holds the downloaded archive files. You can also place unpacked chart directories here for local development -- Helm treats both formats identically.
Subchart Value Overrides
Every subchart has its own values.yaml with its own defaults. The parent chart can override any of those values by nesting them under a key that matches the dependency name:
# Parent values.yaml
postgresql:
auth:
username: myapp
database: myapp_production
primary:
persistence:
size: 50Gi
storageClass: gp3
redis:
architecture: standalone
auth:
enabled: false
master:
persistence:
size: 5Gi
This layering means you never need to fork a subchart just to change a setting. The parent chart acts as the configuration authority, and the subchart templates consume whatever values they receive.
How Value Merging Works
Helm performs a deep merge. The subchart's default values.yaml forms the base, and the parent's overrides are merged on top. For maps, keys are merged recursively. For scalars and arrays, the parent's value replaces the child's entirely.
Global Values
Sometimes several subcharts need the same piece of configuration -- a shared image registry, a storage class, or a domain name. Rather than duplicating the value under every subchart key, Helm supports global values:
global:
storageClass: "gp3"
imageRegistry: "registry.example.com"
imagePullSecrets:
- name: registry-creds
Any chart -- parent or subchart -- can access these via .Values.global.storageClass, .Values.global.imageRegistry, etc. Global values are the recommended mechanism for cross-cutting configuration that must be consistent across the entire release.
Many Bitnami charts already honour global.storageClass and global.imageRegistry out of the box, so setting these globals automatically configures all your Bitnami dependencies.
Conditions and Tags
Not every dependency is needed in every environment. A local development setup might skip Redis, while production needs it. Helm provides two mechanisms to toggle dependencies:
Conditions
A condition is a dot-path into the parent's values that must resolve to a boolean. When set to false, the dependency is excluded entirely:
# Chart.yaml
dependencies:
- name: redis
condition: redis.enabled
# values.yaml
redis:
enabled: true # set to false to skip redis
Tags
Tags group multiple dependencies under a single toggle. Every dependency that carries a tag is enabled or disabled together:
# Chart.yaml
dependencies:
- name: postgresql
tags: [database]
- name: mysql
tags: [database]
# values.yaml
tags:
database: true
When both a condition and a tag apply to the same dependency, the condition takes precedence. If the condition key exists in values, the tag is ignored for that dependency.
Library Charts
A library chart is a special chart type that contains only template helpers -- it never renders Kubernetes resources on its own. You declare it by setting type: library in its Chart.yaml:
apiVersion: v2
name: mylib
type: library
version: 1.0.0
Library charts are perfect for standardising labels, annotations, resource naming, and boilerplate across multiple application charts in your organisation. Any chart that lists a library as a dependency can call its named templates with {{ include "mylib.labels" . }}.
Because library charts have type: library, Helm skips them during manifest rendering -- their templates are available but produce no output unless explicitly included.
import-values
The import-values field in a dependency entry lets you pull specific values from a subchart's exports into the parent's value scope:
dependencies:
- name: postgresql
version: "13.2.24"
repository: "https://charts.bitnami.com/bitnami"
import-values:
- child: primary.service
parent: dbService
- child: auth
parent: dbAuth
After resolution, .Values.dbService in the parent chart contains whatever .Values.primary.service holds in the postgresql subchart. This is useful when the parent templates need to reference subchart-internal details -- for example, constructing a database connection string that uses the subchart's service name and port.
Dependency Version Ranges
Helm uses semantic versioning constraints to give you control over which versions are acceptable:
~1.2.0(tilde) -- Allows patch-level updates:>=1.2.0, <1.3.0. Safe for bug fixes.^1.2.0(caret) -- Allows minor and patch updates:>=1.2.0, <2.0.0. Good when you trust the chart maintainer's semver discipline.>=1.0.0 <2.0.0-- Explicit range for full control.1.2.x-- Wildcard shorthand, equivalent to~1.2.0.
In production, prefer tilde ranges (~) or exact versions to avoid surprise breaking changes. Use caret ranges (^) during active development when you want the latest features.
OCI-Based Dependencies
Starting with Helm 3.8, charts can be stored in OCI-compliant container registries (Docker Hub, GitHub Container Registry, Amazon ECR, etc.). To reference an OCI dependency, use the oci:// scheme:
dependencies:
- name: nginx
version: "15.4.4"
repository: "oci://registry-1.docker.io/bitnamicharts"
OCI dependencies do not require helm repo add. Helm pulls them directly from the registry using the OCI distribution spec. This simplifies CI/CD pipelines and aligns Helm chart storage with container image workflows.
Practical Example: Full Application Stack
Putting it all together, here is a realistic Chart.yaml for a web application that depends on PostgreSQL for persistence, Redis for caching, and a shared library chart for common templates:
apiVersion: v2
name: webapp
version: 2.0.0
appVersion: "3.1.0"
dependencies:
- name: common-lib
version: "1.x.x"
repository: "oci://ghcr.io/myorg/charts"
- name: postgresql
version: "~13.2.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
tags: [backend-storage]
import-values:
- child: primary.service
parent: dbService
- name: redis
version: "~18.6.0"
repository: "oci://registry-1.docker.io/bitnamicharts"
condition: redis.enabled
tags: [backend-storage]
With this structure, a developer can install the entire stack with helm install webapp ./webapp, disable Redis for local dev with --set redis.enabled=false, or switch storage classes globally with --set global.storageClass=standard.
Key Takeaways
- Dependencies let you compose applications from proven, community-maintained charts.
- Use
helm dependency updateduring development andhelm dependency buildin CI for reproducibility. - Override subchart values by nesting under the dependency name; use
global:for shared config. - Conditions and tags give you per-environment flexibility without chart modifications.
- Library charts centralise shared templates across your organisation.
- Version ranges and OCI registries round out a mature dependency management workflow.