A Helm subchart is simply a chart that lives inside another chart. That one sentence hides most of the questions people actually have when they start using them: where the subchart comes from, how its values get set, what it can and cannot see from the parent, how to turn it off, and how to deploy the same subchart twice with different configuration.
This guide covers all of it, starting from the basics and ending with the pattern this post was originally written about: running multiple instances of the same subchart with alias. Everything here applies to charts with apiVersion: v2, which is what Helm 3 and Helm 4 use. For the wider picture of how charts, repositories and releases fit together, start with the complete Helm charts guide.
What Is a Helm Subchart?
A subchart is a complete, standalone chart that another chart (the parent, sometimes called an umbrella chart) includes and renders as part of the same release. When you run helm install on the parent, Helm renders the parent’s templates and every enabled subchart’s templates together, and all resulting objects belong to a single release with a single revision history.
There are two ways a chart can become a subchart.
Declared in Chart.yaml (the normal way)
You list it in the dependencies section of the parent’s Chart.yaml, and Helm downloads it into the charts/ directory for you:
# Chart.yaml
apiVersion: v2
name: shop
version: 1.4.0
dependencies:
- name: postgresql
version: "~16.2.0"
repository: "https://charts.bitnami.com/bitnami"
- name: redis
version: "~20.1.0"
repository: "oci://registry-1.docker.io/bitnamicharts"Dropped manually into charts/
Any chart placed in the parent’s charts/ directory, either unpacked as a folder or as a .tgz archive, is treated as a dependency even if Chart.yaml does not mention it. Directory and file names starting with _ or . are ignored by the loader. This is handy for vendoring a chart you forked, but you lose version constraints and the lock file, so it is best reserved for special cases.
Subchart vs dependency: is there a difference?
In practice, no. “Dependency” is the declaration in Chart.yaml, “subchart” is the chart that ends up in charts/ and gets rendered. Every dependency becomes a subchart once it is fetched, and every subchart is a dependency of its parent. The distinction people usually mean when they search for “helm subchart vs dependency” is actually declared dependency vs manually vendored chart, which is the comparison above. If you want the full reference of every field in the dependencies block and how version ranges are resolved, see the Helm dependencies guide and the post on Helm version constraints.
helm dependency update, build and Chart.lock
Declaring a dependency does nothing on its own. You have to fetch it:
helm dependency update ./shopupdate resolves each version range against the repository, downloads the matching archives into charts/, and writes Chart.lock with the exact versions and a digest of the dependency list. Run it whenever you change dependencies in Chart.yaml.
helm dependency build ./shopbuild does the reproducible thing: it rebuilds charts/ from the versions pinned in Chart.lock without re-resolving ranges. This is what you want in CI.
Two errors you will see sooner or later:
found in Chart.yaml, but missing in charts/ directorymeans you declared a dependency and never fetched it. Runhelm dependency build(orupdate).the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml)means someone editedChart.yamlwithout updating the lock. Runhelm dependency updateand commit the newChart.lock.
Commit Chart.lock. Whether to commit the charts/*.tgz files is a team choice: committing them makes builds hermetic, not committing them keeps the repository small and relies on helm dependency build in the pipeline.
Passing Values to a Subchart
This is the part that confuses people the most, and it follows one rule: values for a subchart go under a top-level key named after the subchart (or after its alias, as we will see later).
# shop/values.yaml
replicaCount: 3 # parent value
postgresql: # everything under this key goes to the postgresql subchart
auth:
database: shop
username: shop
primary:
persistence:
size: 20Gi
redis:
architecture: standaloneInside the postgresql subchart, templates see .Values.auth.database, not .Values.postgresql.auth.database. Helm strips the prefix and hands the subchart only its own slice. Values you set in the parent under that key are merged on top of the subchart’s own values.yaml defaults, so you only need to write what you want to change.
The same works on the command line:
helm install shop ./shop \
--set postgresql.primary.persistence.size=50Gi \
--set redis.architecture=replicationTo remove a default the subchart ships with, rather than overriding it, set it to null in the parent. The details and the edge cases of that are covered in Helm null values.
The parent can read subchart values, not the other way around
The parent’s templates can reference .Values.postgresql.auth.database because that key exists in the parent’s value tree. The subchart cannot reference replicaCount from the parent: it only receives its own section plus global. This is intentional. A subchart must render the same way whether it is installed alone or embedded, so it cannot depend on anything its parent happens to define.
Global Values
global is the one channel that reaches every chart in the tree:
# shop/values.yaml
global:
imageRegistry: registry.example.com
storageClass: fast-ssdEvery chart, parent and subcharts, can read .Values.global.imageRegistry. Globals flow downwards and parent globals take precedence over any global a subchart defines in its own values.yaml.
Two practical warnings. First, a global only does something if the subchart’s templates actually read it. Bitnami charts, for example, honour global.imageRegistry and global.storageClass because their templates were written to; a random chart may ignore both. Check the subchart’s templates or README before relying on a global. Second, do not use global as a general-purpose way to share configuration between your own charts. It works, but it creates an invisible contract between charts that is hard to trace months later. Explicit per-subchart values are easier to review.
Enabling and Disabling Subcharts: condition and tags
Every declared dependency is rendered by default. To make one optional you add a condition, a tags list, or both.
# Chart.yaml
dependencies:
- name: postgresql
version: "~16.2.0"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: prometheus-exporter
version: "1.x.x"
repository: "https://example.com/charts"
tags:
- monitoring
- name: grafana-dashboards
version: "1.x.x"
repository: "https://example.com/charts"
tags:
- monitoring# values.yaml
postgresql:
enabled: true
tags:
monitoring: falseThe resolution rules, from the Helm documentation:
- A
conditionis a YAML path into the parent’s values that must resolve to a boolean. You can list several comma-separated paths; the first one that exists wins and the rest are ignored. tagsenable a chart if any of its tags is true.- When both are present and the condition path is set in values, the condition always overrides tags.
- If neither resolves, the chart is enabled.
The usual pattern is a condition per subchart for fine control and a shared tag for groups that should switch on and off together, such as everything related to monitoring:
helm install shop ./shop --set tags.monitoring=true --set postgresql.enabled=falseA common mistake is to write condition: enabled instead of condition: postgresql.enabled. The path is resolved from the parent’s root, not from inside the subchart’s section.
import-values: Pulling Subchart Values Up to the Parent
Values normally flow down. import-values lets the parent pull specific values up from a subchart, which is useful when the subchart knows something the parent’s templates need, such as a port or a service name.
The exports format
The subchart publishes values under a top-level exports key:
# charts/api/values.yaml
exports:
data:
servicePort: 8080The parent imports them by key:
# Chart.yaml (parent)
dependencies:
- name: api
version: "2.x.x"
repository: "https://example.com/charts"
import-values:
- dataAfter rendering, the parent sees .Values.servicePort at its root. The contents of exports.data are merged into the parent’s root values, not under a data key.
The child-parent format
When the subchart does not export anything, you can map any path explicitly:
dependencies:
- name: api
version: "2.x.x"
repository: "https://example.com/charts"
import-values:
- child: service
parent: apiServiceNow .Values.apiService.port in the parent holds whatever the subchart had in service.port. Imported values override defaults in the parent’s values.yaml, but values passed at install time with -f or --set still win.
Use import-values sparingly. If the parent needs a value that the subchart also needs, it is often clearer to define it once in the parent and pass it down, or put it in global.
What a Subchart Cannot Do
Knowing the limits saves a lot of debugging time:
- It cannot see parent values. Only its own section and
global. There is no.Parent.Values. - It cannot override parent values. Values flow down;
import-valuesis the only way up, and the parent opts into it. - Named templates are global. Everything defined with
{{ define }}across the parent and all subcharts shares one namespace. If two charts definemychart.labels, one silently wins. That is why well-written charts prefix every template with the chart name, and whyhelm creategenerates names like{{ include "shop.fullname" . }}. - It is part of the same release. You cannot upgrade, roll back or uninstall a subchart independently. A rollback of the parent rolls back every subchart.
- Hooks run in the same release. Subchart hooks execute alongside the parent’s, ordered by weight, which can surprise you if two charts both ship a pre-install migration job. See the Helm hooks guide for ordering rules.
Multiple Instances of the Same Subchart with alias
Now to the less common but very useful case: you want the same subchart twice, each copy with its own configuration.
When you need it
The scenario that led me to this pattern was a set of microservices that belong to one application and share the same technology base. All of them could use the same generic chart (in my case a TIBCO BusinessWorks Container Edition chart; it could just as well be a generic go-microservice or spring-boot chart), but each one needs:
- its own image,
- its own configuration and environment variables,
- its own endpoints, often talking to different databases or external systems.
Without subcharts you end up copying the chart once per service, and every fix to the base chart has to be applied N times. With one subchart instantiated N times, the base chart is versioned once and each service is just a block of values.
Other real cases: two Redis instances (one cache, one queue) with different persistence settings, or a primary and a reporting database from the same PostgreSQL chart.
Declaring the instances
Start from a parent that includes the generic chart once:
# Chart.yaml
apiVersion: v2
name: orders-platform
description: Orders application deployed from a shared service chart
version: 0.2.0
appVersion: "2.7.2"
dependencies:
- name: service
version: "~0.2.0"
repository: "https://charts.example.com"To have two instances, declare the same chart twice and give each an alias:
# Chart.yaml
apiVersion: v2
name: orders-platform
description: Orders application deployed from a shared service chart
version: 0.3.0
appVersion: "2.7.2"
dependencies:
- name: service
alias: order-api
version: "~0.2.0"
repository: "https://charts.example.com"
condition: order-api.enabled
- name: service
alias: order-worker
version: "~0.2.0"
repository: "https://charts.example.com"
condition: order-worker.enabledname stays the same, so helm dependency update downloads the chart once. The alias is what makes each copy distinct: it becomes the values key for that instance and it becomes .Chart.Name inside the instance’s templates.
Configuring each instance
Each alias gets its own section in values.yaml:
# values.yaml
order-api:
enabled: true
image:
repository: registry.example.com/orders/order-api
tag: "2.5.2"
pullPolicy: IfNotPresent
service:
port: 8080
env:
DB_HOST: orders-db.internal
order-worker:
enabled: true
replicaCount: 2
image:
repository: registry.example.com/orders/order-worker
tag: "2.5.2"
env:
QUEUE_URL: amqp://queue.internalAnything you do not set falls back to the shared chart’s defaults, so each block contains only what makes that service different.
Three things that bite with aliases
Use lowercase, DNS-safe aliases. Because the alias replaces .Chart.Name, the standard fullname helper builds resource names from it. An alias like serviceA (which the first version of this post used) produces names such as myrelease-serviceA, which Kubernetes rejects because object names must be lowercase. Stick to order-api style names.
Hyphenated aliases need index in parent templates. If the parent’s own templates need to read an instance’s values, .Values.order-api.service.port is a template parse error. Use {{ index .Values "order-api" "service" "port" }} instead. Templates inside the subchart are unaffected, because they see their own slice without the prefix.
The shared chart must use its chart name in resource names. If the base chart hard-codes name: service in its Deployment, both instances render an object with the same name and the install fails. A chart designed for reuse builds names from include "service.fullname", which derives from .Chart.Name and therefore from the alias.
Library Charts vs Subcharts
A library chart (type: library in Chart.yaml) is also declared as a dependency, but it renders nothing by itself. It only provides named templates the parent can include.
| Subchart (application) | Library chart | |
|---|---|---|
| Renders Kubernetes objects | Yes | No |
Has its own values.yaml section | Yes | No, uses the caller’s context |
| Can be installed alone | Yes | No |
| Typical use | Bundling a database, cache or service | Shared labels, Deployment skeletons, helpers |
| Multiple instances | With alias | Not needed: call the template N times |
For the multi-microservice case above there is a real alternative: a library chart that defines a full Deployment/Service template, and a parent that calls it once per service from a range over values. That keeps every service in the parent’s own templates and avoids alias quirks, at the cost of writing a bit more template code. Aliased subcharts are simpler when the shared chart already exists as an installable chart; library charts are cleaner when you design the abstraction from scratch.
helm upgrade with Subcharts
Upgrading a release that contains subcharts is the same helm upgrade, with two details worth knowing.
If you bump a dependency version, you must refresh charts/ before upgrading, either explicitly or with the flag:
helm dependency update ./shop
helm upgrade shop ./shop -f values-prod.yaml
# or in one step: fetches dependencies if they are missing
helm upgrade shop ./shop -f values-prod.yaml --dependency-updateNote that --dependency-update only fetches dependencies that are missing; it does not replace an older archive that is already sitting in charts/. After changing a version range, run helm dependency update explicitly.
Values behave as in any upgrade. Without --reuse-values, Helm starts from the chart’s defaults plus whatever you pass on this command; that is usually what you want, because a new subchart version may have renamed or added keys. --reuse-values merges your overrides on top of the previous release’s values and can hide new subchart defaults. --reset-then-reuse-values is the middle ground: chart defaults first, then the last release’s values, then your overrides. Before a subchart major version bump, render both versions with helm template and diff them. The advanced Helm commands post has more on inspecting a release before touching it.
Umbrella Charts: When to Use Them and When to Avoid Them
An umbrella chart is a parent whose main job is to bundle several subcharts into one deployable unit. Subcharts make them easy to build, which is exactly why they get overused.
They work well when:
- the components share a lifecycle and are always released together, such as an application and its dedicated cache,
- you want one versioned artifact per environment that says exactly what is deployed,
- the same generic chart is instantiated several times, as in the alias example.
They hurt when:
- components have different release cadences, because every change to one service creates a new revision of all of them and a rollback reverts everything,
- different teams own different components and step on each other’s values,
- the release grows so large that a single failed hook or a single invalid object blocks the whole upgrade.
If you are deciding how to organise charts across many services, the trade-offs between one central chart and per-service charts are covered in detail in Helm chart management: centralized vs per-service. Whatever structure you choose, test the charts with the subcharts enabled, and validate the values each instance receives with a values schema.
Frequently Asked Questions
What is a subchart in Helm?
A subchart is a complete Helm chart included inside another chart, either declared in the parent’s Chart.yaml under dependencies and fetched into charts/, or placed there manually. It is rendered as part of the parent’s release, receives its values from a key named after it in the parent’s values, and cannot see the parent’s own values except for global.
What is the difference between a Helm subchart and a dependency?
They are two views of the same thing. A dependency is the entry in Chart.yaml that says which chart and version to use; the subchart is that chart once it has been downloaded into charts/ and rendered. The only practical distinction is between declared dependencies, which have version ranges and a Chart.lock, and charts copied manually into charts/, which have neither.
How do I pass values to a Helm subchart?
Put them under a top-level key with the subchart’s name, or its alias if it has one, in the parent’s values.yaml, or use --set subchartname.key=value. Helm strips that prefix and merges the section over the subchart’s own defaults, so inside the subchart the values appear at .Values.key. Values that every chart should see go under global.
Can a subchart access the parent chart’s values?
No. A subchart only receives its own section of values and the global section. This keeps subcharts self-contained so they render identically whether installed alone or embedded. If a subchart needs something from the parent, pass it explicitly under the subchart’s key or place it in global.
How do I deploy the same Helm chart multiple times as subcharts?
Declare the same chart several times in dependencies with the same name and a different alias for each entry. Each alias becomes its own values key and its own .Chart.Name, so every instance gets separate configuration and separate resource names. Use lowercase, DNS-safe aliases, and make sure the shared chart builds resource names from its fullname helper rather than hard-coding them.
How do I disable a subchart?
Add condition: <subchart>.enabled to the dependency in Chart.yaml and set <subchart>.enabled: false in values or with --set. Alternatively assign tags and set tags.<tag>: false. If both are defined, a condition that resolves in values always overrides tags.
Do I need to run helm dependency update before helm upgrade?
Yes, whenever you change a dependency version in Chart.yaml. helm upgrade --dependency-update fetches dependencies that are missing from charts/, but it does not replace an archive that is already there, so after bumping a version run helm dependency update and commit the new Chart.lock. In CI, helm dependency build recreates charts/ from the lock file.
What is the difference between a library chart and a subchart?
A subchart is an application chart that renders its own Kubernetes objects and has its own values section. A library chart, marked with type: library, renders nothing and only provides named templates for the parent to include. Use subcharts to bundle components, and library charts to share template code across many charts.
Conclusion
Subcharts are Helm’s composition mechanism, and most of their behaviour follows from two ideas: values flow down under a key named after the subchart, and everything renders into one release. Once that is clear, global, condition, tags and import-values are just controlled exceptions to the flow.
The multi-instance pattern with alias builds on the same rules. It lets you keep one well-tested generic chart and instantiate it for every service that shares the same base, each with its own values, instead of maintaining a copy per service. Use it when the instances genuinely share a lifecycle, keep aliases lowercase, and reach for per-service releases or a library chart when they do not. That keeps the benefits of reuse without turning your umbrella chart into the monolith containers were supposed to get rid of.