Subcharts & dependencies
Compose charts; share values.
A recipe that calls for two cups of tomato sauce does not make you simmer tomatoes for an hour. You reach for a jar someone already made, trust the label, and get on with the dish. A Helm chart can work the same way. When your application needs a PostgreSQL database (a relational database that stores data in tables) and a Redis cache (an in-memory store that holds data you need to reach fast), you do not rewrite those from raw Kubernetes YAML (Kubernetes is the system that runs your containers across a pool of machines; YAML is the indentation-based text format it reads). You declare them as dependencies, and Helm fetches their charts for you. A chart pulled in this way is called a subchart. The chart doing the pulling is the parent.
Dependencies live in one place: the dependencies list in the parent's Chart.yaml (the file that holds a chart's name, version, and metadata). This is how large applications get assembled out of parts other people maintain, the way an operating system package pulls in the libraries it needs instead of bundling its own copy of everything.
Declaring what your chart depends on
Each entry in the list has a handful of fields, and every one of them matters for security later. name is the subchart's name, and it doubles as the key you use to configure it. version is a version constraint written in semantic versioning (the major.minor.patch scheme), and it can be one exact version like 15.5.38 or a range like 15.5.x that means "any patch of 15.5". repository says where to fetch the chart from, either an HTTP chart repository or an OCI registry (Open Container Initiative, the standard that container registries speak) addressed with an oci:// URL. condition points at a boolean value that switches the whole subchart on or off. tags are named groups, so one switch can flip several subcharts together.
apiVersion: v2name: payments-apiversion: 1.4.0appVersion: "2.1.0"dependencies:- name: postgresqlversion: "15.5.x" # a range: any patch of 15.5repository: https://charts.bitnami.com/bitnamicondition: postgresql.enabled # switch the whole subchart on/off- name: redisversion: "19.x.x"repository: https://charts.bitnami.com/bitnamicondition: redis.enabledtags:- cache # flip every subchart tagged "cache" at once
That condition field is the difference between a chart that only works one way and one you can actually run in more than one place. In development you set postgresql.enabled to true and get a throwaway database inside the cluster (the pool of machines Kubernetes runs your workloads on). In production you set it to false and point the app at a managed database your platform team runs, with backups and patching handled for you. Same chart, two very different deployments, no forked copies.
Two more fields turn up in bigger charts. alias lets you list the same subchart twice under different names, so one parent can run two independent Redis instances side by side. import-values pulls data the other direction, letting a subchart expose some of its computed values up to the parent. You will not reach for either on day one, but they are how the same building blocks compose into genuinely large charts.
Fetching them: update versus build
Think of a shopping list versus the itemized receipt you get at the till. helm dependency update works from the list. It reads Chart.yaml, resolves each version constraint to one concrete release, downloads each subchart as a compressed .tgz archive into the charts/ directory, and writes Chart.lock. That lock file is the receipt: it records the exact versions it settled on plus a SHA-256 checksum (a cryptographic fingerprint) of the whole set.
$ helm dependency update ./payments-api
Hang tight while we grab the latest from your chart repositories......Successfully got an update from the "bitnami" chart repositoryUpdate Complete. ⎈Happy Helming!⎈Saving 2 chartsDownloading postgresql from repo https://charts.bitnami.com/bitnamiDownloading redis from repo https://charts.bitnami.com/bitnamiDeleting outdated charts
The archives land in charts/, and the versions the ranges resolved to get frozen into Chart.lock. Read the lock file and you can see exactly what was pulled, down to the patch release.
$ ls charts/
postgresql-15.5.38.tgz redis-19.6.4.tgz
dependencies:- name: postgresqlrepository: https://charts.bitnami.com/bitnamiversion: 15.5.38- name: redisrepository: https://charts.bitnami.com/bitnamiversion: 19.6.4digest: sha256:3b1c0e7a9f2d4b6c8e0a1f3d5b7c9e1a2f4d6b8c0e2a4f6d8b0c2e4a6f8d0b2cgenerated: "2026-07-17T10:22:04.481233+02:00"
helm dependency build works the other way around. It ignores the ranges in Chart.yaml and fetches exactly what Chart.lock already names, refusing to run if the two files disagree. If you have worked in a JavaScript project, this is the same split as npm install (resolve fresh) versus npm ci (obey the lockfile). Use update on your workstation when you deliberately want newer subcharts. Use build in CI (continuous integration, the automation that rebuilds and tests your code every time it changes) so every pipeline run assembles the identical set of charts.
$ helm dependency list ./payments-api
NAME VERSION REPOSITORY STATUSpostgresql 15.5.x https://charts.bitnami.com/bitnami okredis 19.x.x https://charts.bitnami.com/bitnami ok
That STATUS column is worth a glance in CI. ok means the archive in charts/ matches the lock, while missing or wrong version means someone changed Chart.yaml and never re-fetched. In that drifted state, build stops rather than guessing, which is exactly what you want a pipeline to do.
$ helm dependency build ./payments-api # after Chart.yaml was edited but the lock was not
Error: the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml). Please update the dependencies
Handing values down to a subchart
Picture the mailboxes in an apartment lobby. Each subchart has one, labelled with its name. Anything you write under the postgresql key in the parent's values file gets delivered into the postgresql subchart and read there as if it were that chart's own top-level configuration. There is also one shared notice board, the global block, that every subchart can read. Those are the only two routes in. A value you drop at the parent's top level, not under a subchart's name and not under global, never reaches any subchart at all.
postgresql: # delivered to the postgresql subchartenabled: trueauth:database: paymentsexistingSecret: pg-credentials # read a Secret you made; do not invent a passwordprimary:persistence:size: 20Giredis: # delivered to the redis subchartenabled: truearchitecture: standaloneglobal: # the only block every subchart can readimageRegistry: registry.internalstorageClass: fast-ssd
So postgresql.auth.database sets the database name inside the postgresql subchart, and postgresql.auth.existingSecret tells that subchart to read a password from a Kubernetes Secret (the Kubernetes object that holds sensitive values like passwords) you created ahead of time, instead of generating one and writing it back into the release (Helm's name for one installed instance of a chart). global.imageRegistry reaches both subcharts and the parent, which is how you make everything pull images from your own internal registry instead of the public internet in one line.
Know exactly what a subchart deploys
A subchart is code, not a config knob you turn. It is other people's templates, and installing the parent applies whatever those templates render, using your credentials against your cluster. A community database chart can stand up a StatefulSet (the Kubernetes controller that runs pods with stable names and storage; a pod is the smallest unit Kubernetes schedules, one or more containers that live and die together), its own ServiceAccount (the identity a pod uses to talk to the Kubernetes API) and RBAC rules (role-based access control, which decides what an identity is allowed to do), NetworkPolicies (firewall rules that decide which pods may talk to which), and Secrets, none of which you wrote by hand. Before you trust one in production, render the whole thing and read it. helm template prints every manifest (the YAML description of a single Kubernetes object) the chart would apply, without ever touching the cluster.
$ helm template payments ./payments-api | grep -E '^kind:' | sort | uniq -c
1 kind: ConfigMap1 kind: Deployment2 kind: NetworkPolicy1 kind: Secret3 kind: Service2 kind: ServiceAccount2 kind: StatefulSet
That one line of output is the honest inventory of your "app". Two StatefulSets and a Secret you never authored, ServiceAccounts that will hold real permissions in your cluster, NetworkPolicies that quietly decide what is allowed to talk to what. Read the actual manifests for anything that runs privileged, mounts a host path, or grants broad RBAC. helm template ./payments-api --show-only charts/postgresql/templates/statefulset.yaml narrows the output to a single file when you want to inspect one piece closely.
Pinning is not the same as trusting
Freezing a version stops the code from changing under you. It does not make the code safe, and it does not guarantee the code will still be there tomorrow. Two habits close most of the gap. First, pin exact subchart versions or commit Chart.lock, and run helm dependency build in CI so a deploy never silently picks up a newer subchart. Second, vendor the archives: commit the charts/*.tgz files into your repository so the exact bytes you reviewed are the exact bytes you ship, with no live fetch at deploy time.
That second habit stopped being theoretical in the second half of 2025. Bitnami restructured its public catalog, moving older image versions out of the default location into a separate bitnamilegacy repository and putting its hardened images behind a paid tier. Teams that had pinned old chart versions found the chart still resolved, but the container images those charts pointed at had moved, and pods failed to pull. A version pin in Chart.yaml protects the chart. It says nothing about whether the registry will keep serving what the chart points at.
So build one more habit in. An unpinned subchart pulls whatever the repository serves today, the same moving-target risk as any unpinned dependency, except this one deploys workloads straight into your cluster. Pin exact versions, commit Chart.lock so helm dependency build reproduces the identical set, and for anything you cannot afford to have vanish, vendor the .tgz archives and mirror the images into a registry you control. Review what each subchart deploys before you trust it, and re-review every time you bump the version.
Try this
Run helm dependency update ./payments-api on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: a subchart only reads its own key and global. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.