Helm Subcharts Explained: Values, Globals, import-values and Multiple Instances with Alias

Helm Subcharts Explained: Values, Globals, import-values and Multiple Instances with Alias

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 ./shop

update 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 ./shop

build 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/ directory means you declared a dependency and never fetched it. Run helm dependency build (or update).
  • the lock file (Chart.lock) is out of sync with the dependencies file (Chart.yaml) means someone edited Chart.yaml without updating the lock. Run helm dependency update and commit the new Chart.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: standalone

Inside 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=replication

To 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-ssd

Every 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: false

The resolution rules, from the Helm documentation:

  • A condition is 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.
  • tags enable 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=false

A 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: 8080

The parent imports them by key:

# Chart.yaml (parent)
dependencies:
  - name: api
    version: "2.x.x"
    repository: "https://example.com/charts"
    import-values:
      - data

After 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: apiService

Now .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-values is 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 define mychart.labels, one silently wins. That is why well-written charts prefix every template with the chart name, and why helm create generates 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.enabled

name 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.internal

Anything 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 objectsYesNo
Has its own values.yaml sectionYesNo, uses the caller’s context
Can be installed aloneYesNo
Typical useBundling a database, cache or serviceShared labels, Deployment skeletons, helpers
Multiple instancesWith aliasNot 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-update

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

Helm Dependencies Explained: How Chart Dependencies Work in Helm

Helm Dependencies Explained: How Chart Dependencies Work in Helm

Helm Dependency is a critical part of understanding how Helm works as it is the way to establish relationships between different helm packages. We have talked a lot here about what Helm is, and some topics around that, and we even provided some tricks if you create your charts.

Related reading: Helm chart management: centralized repos vs per-service charts.

Related reading: Helm version constraints: tilde, caret and SemVer ranges.

Understanding chart dependencies is crucial for building scalable Helm architectures. Explore more Helm patterns and best practices in our comprehensive Helm guide.

So, as commented, Helm Chart is nothing more than a package that you put around the different Kubernetes objects that need to be deployed for your application to work. The usual comparison is that it is similar to a Software Package. When you install an application that depends on several components, all of those components are packaged together, and here is the same thing.

What is a Helm Dependency?

A Helm Dependency is nothing more than the way you define that your Chart needs another chart to work. For sure, you can create a Helm Chart with everything you need to deploy your application, but something you would like to split that work into several charts just because they are easy to maintain or the most common use case because you want to leverage another Helm Chart that is already available.

One use case can be a web application that requires a database, so you can create on your Helm Chart all the YAML files to deploy your web application and your Database in Kubernetes, or you can have your YAML files for your web application (Deployment, Services, ConfigMaps,…) and then say: And I need a database and to provide it I’m going to use this chart.

This is similar to how it works with the software packages in UNIX systems; you have your package that does the job, like, for example, A, but for that job to be done, it requires the library L, and to ensure that when you are installing A, Library L is already there or if not it will be installed you declare that your application A depends on Library L, so here is the same thing. You declare that your Chart depends on another Chart to work. And that leaves us to the next point.

How do we declare a Helm Dependency?

This is the next point; now that we understand what a Helm Dependency is conceptually and we have a use case, how can we do that in our Helm Chart?

All the work is done in the Chart.yml file. If you remember, the Chart.yml file is the file where you declare all the metadata of your Helm Chart, such as the name, the version of the chart, the application version, location URL, icon, and much more. And usually has a structure like this one:

apiVersion: v2
name: MyChart
description: My Chart Description
type: application
version: 0.2.0
appVersion: "1.16.0"

So here we can add a section dependencies and, in that section is where we are going to define the charts that we depend on. As you can see in the snippet below:

apiVersion: v2
name: MyChart
description: My Chart Description
type: application
version: 0.2.0
appVersion: "1.16.0"
dependencies:
- name: Dependency
  version: 1.0.0
  repository: "file:///location_of_my_chart"

Here we are declaring Dependency as our Helm Dependency. We specify the version that we would like to use (similar to the version we say in our chart), and that will help us to ensure that we will provide the same version that has been tested as part of the resolution of the dependency and also the location using an URL that can be an external URL is this is pointing to a Helm Chart that is available on the internet or outside your computer or using a File Path in case you are pointing to a local resource in your machine.

That will do the job of defining the helm dependency, and this way, when you install your chart using the command helm install, it will also provide the dependence.

How do I declare a Helm Conditional Dependency?

Until now, we learned how to declare a dependency, and each time I provision my application, it will also provide the dependence. But usually, we would like to have a fine-grained approach to that. Imagine the same scenario as above: We have our Web Application that depends on the Database, and we have two options, we can provision the database as part of the installation of the web application, or we can point to an external database and in that case, it makes no sense to provision the Helm Dependency. How can we do that?

So, easy, because one of the optional parameters you can add to your dependency is condition and do exactly that, condition allow you to specify a flag in your values.yml that in the case is equal to true, it will provide the dependency but in the case is equal to false it will skip that part similar to the snippet shown below:

 apiVersion: v2
name: MyChart
description: My Chart Description
type: application
version: 0.2.0
appVersion: "1.16.0"
dependencies:
- name: Dependency
  version: 1.0.0
  repository: "file:///location_of_my_chart"
  condition: database.enabled 

And with that, we will set the enabled parameter under database in our values.yml to true if we would like to provision it.

How do I declare a Helm Dependency With a Different version?

As shown in the snippets above, we offer that when we declare a Helm Dependency, we specify the version; that is a safe way to do it because it ensures that any change done to the helm chart will not affect your package. Still, at the same time, you cannot be aware of security fixes or patches to the chart that you would like to leverage in your deployment.

To simplify that, you have the option to define the version in a more flexible way using the operator ~ in the definition of the version, as you can see in the snippet below:

apiVersion: v2
name: MyChart
description: My Chart Description
type: application
version: 0.2.0
appVersion: "1.16.0"
dependencies:
- name: Dependency
  version: ~1.0.0
  repository: "file:///location_of_my_chart"
  condition: database.enabled 

This means that any patch done to the chart will be accepted, so this is similar that this chart will use the latest version of 1.0.X. Still, it will not use the 1.1.0 version, so that allows to have more flexibility, but at the same time keeping things safe and secured in case of a breaking change on the Chart you depend on. This is just one way to define that, but the flexibility is enormous as the Chart versions use “Semantic Versions,” You can learn and read more about that here: https://github.com/Masterminds/semver.