Deploy Kubernetes Applications with Helm Charts

The kubernetes:helm.sh/v4:Chart resource, part of the Pulumi Kubernetes provider, renders Helm chart templates and manages the resulting Kubernetes resources directly through Pulumi. This guide focuses on three capabilities: chart sources (local directories, repositories, OCI registries), values customization, and namespace placement.

Charts require a configured Kubernetes cluster and may reference Helm repositories or local chart directories. The Chart resource does not use Tiller or create a Helm Release; it renders templates (equivalent to helm template --dry-run=server) and deploys the resulting manifests. The examples are intentionally small. Combine them with your own values files, namespaces, and cluster configuration.

Deploy a chart from a local directory

Teams developing or customizing charts often work with unpacked directories on disk, allowing iteration on templates before publishing.

import * as k8s from "@pulumi/kubernetes";

const nginx = new k8s.helm.v4.Chart("nginx", {
    chart: "./nginx",
});
import pulumi
from pulumi_kubernetes.helm.v4 import Chart

nginx = Chart("nginx",
    chart="./nginx"
)
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
			Chart: pulumi.String("./nginx"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V4;
using System.Collections.Generic;

return await Deployment.RunAsync(() =>
{
    new Pulumi.Kubernetes.Helm.V4.Chart("nginx", new ChartArgs
    {
        Chart = "./nginx"
    });
    return new Dictionary<string, object?>{};
});
package generated_program;

import com.pulumi.Pulumi;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var nginx = new Chart("nginx", ChartArgs.builder()
                    .chart("./nginx")
                    .build());
        });
    }
}
name: example
runtime: yaml
resources:
  nginx:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      chart: ./nginx

The chart property accepts a path to a local directory containing a valid Helm chart. If a Chart.lock file exists and dependencies are missing, Pulumi automatically rebuilds them. This approach works well during development when you’re modifying chart templates directly.

Pull a chart from a Helm repository

Production deployments typically fetch charts from public or private repositories, ensuring consistent versions across environments.

import * as k8s from "@pulumi/kubernetes";

const nginx = new k8s.helm.v4.Chart("nginx", {
    chart: "nginx",
    repositoryOpts: {
        repo: "https://charts.bitnami.com/bitnami",
    },
});
import pulumi
from pulumi_kubernetes.helm.v4 import Chart,RepositoryOptsArgs

nginx = Chart("nginx",
    chart="nginx",
    repository_opts=RepositoryOptsArgs(
        repo="https://charts.bitnami.com/bitnami",
    )
)
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
			Chart: pulumi.String("nginx"),
			RepositoryOpts: &helmv4.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.bitnami.com/bitnami"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V4;
using System.Collections.Generic;

return await Deployment.RunAsync(() =>
{
    new Pulumi.Kubernetes.Helm.V4.Chart("nginx", new ChartArgs
    {
        Chart = "nginx",
        RepositoryOpts = new RepositoryOptsArgs
        {
            Repo = "https://charts.bitnami.com/bitnami"
        },
    });
    
    return new Dictionary<string, object?>{};
});
package generated_program;

import com.pulumi.Pulumi;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;
import com.pulumi.kubernetes.helm.v4.inputs.RepositoryOptsArgs;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var nginx = new Chart("nginx", ChartArgs.builder()
                    .chart("nginx")
                    .repositoryOpts(RepositoryOptsArgs.builder()
                            .repo("https://charts.bitnami.com/bitnami")
                            .build())
                    .build());
        });
    }
}
name: example
runtime: yaml
resources:
  nginx:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      chart: nginx
      repositoryOpts:
        repo: https://charts.bitnami.com/bitnami

The repositoryOpts property specifies the Helm repository URL. Pulumi fetches the latest stable version unless you specify a version. This example pulls the nginx chart from Bitnami’s public repository.

Pull a chart from an OCI registry

OCI registries provide an alternative distribution method, using the same infrastructure as container images.

import * as k8s from "@pulumi/kubernetes";

const nginx = new k8s.helm.v4.Chart("nginx", {
    chart: "oci://registry-1.docker.io/bitnamicharts/nginx",
    version: "16.0.7",
});
import pulumi
from pulumi_kubernetes.helm.v4 import Chart

nginx = Chart("nginx",
    chart="oci://registry-1.docker.io/bitnamicharts/nginx",
    version="16.0.7",
)
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
			Chart:   pulumi.String("oci://registry-1.docker.io/bitnamicharts/nginx"),
			Version: pulumi.String("16.0.7"),
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V4;
using System.Collections.Generic;

return await Deployment.RunAsync(() =>
{
    new Pulumi.Kubernetes.Helm.V4.Chart("nginx", new ChartArgs
    {
        Chart = "oci://registry-1.docker.io/bitnamicharts/nginx",
        Version = "16.0.7",
    });
    
    return new Dictionary<string, object?>{};
});
package generated_program;

import com.pulumi.Pulumi;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var nginx = new Chart("nginx", ChartArgs.builder()
                    .chart("oci://registry-1.docker.io/bitnamicharts/nginx")
                    .version("16.0.7")
                    .build());
        });
    }
}
name: example
runtime: yaml
resources:
  nginx:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      chart: oci://registry-1.docker.io/bitnamicharts/nginx
      version: "16.0.7"

The chart property accepts an oci:// URL pointing to a chart in an OCI-compatible registry. The version property pins the chart to a specific release. OCI charts require explicit version specification.

Override chart values with files and maps

Charts expose configuration through values that control resource sizing, service types, and application behavior.

import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";

const nginx = new k8s.helm.v4.Chart("nginx", {
    chart: "nginx",
    repositoryOpts: {
        repo: "https://charts.bitnami.com/bitnami",
    },
    valueYamlFiles: [
        new pulumi.asset.FileAsset("./values.yaml")
    ],
    values: {
        service: {
            type: "ClusterIP",
        },
        notes: new pulumi.asset.FileAsset("./notes.txt"),
    },
});
"""A Kubernetes Python Pulumi program"""

import pulumi
from pulumi_kubernetes.helm.v4 import Chart,RepositoryOptsArgs

nginx = Chart("nginx",
    chart="nginx",
    repository_opts=RepositoryOptsArgs(
        repo="https://charts.bitnami.com/bitnami"
    ),
    value_yaml_files=[
        pulumi.FileAsset("./values.yaml")
    ],
    values={
        "service": {
            "type": "ClusterIP"
        },
        "notes": pulumi.FileAsset("./notes.txt")
    }
)
package main

import (
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		_, err := helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
			Chart: pulumi.String("nginx"),
			RepositoryOpts: &helmv4.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.bitnami.com/bitnami"),
			},
			ValueYamlFiles: pulumi.AssetOrArchiveArray{
				pulumi.NewFileAsset("./values.yaml"),
			},
			Values: pulumi.Map{
				"service": pulumi.Map{
					"type": pulumi.String("ClusterIP"),
				},
				"notes": pulumi.NewFileAsset("./notes.txt"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Helm.V4;
using System.Collections.Generic;

return await Deployment.RunAsync(() =>
{
    new Pulumi.Kubernetes.Helm.V4.Chart("nginx", new ChartArgs
    {
        Chart = "nginx",
        RepositoryOpts = new RepositoryOptsArgs
        {
            Repo = "https://charts.bitnami.com/bitnami"
        },
        ValueYamlFiles = 
        {
            new FileAsset("./values.yaml") 
        },
        Values = new InputMap<object>
        {
            ["service"] = new InputMap<object>
            {
                ["type"] = "ClusterIP",
            },
            ["notes"] = new FileAsset("./notes.txt")
        },
    });
    
    return new Dictionary<string, object?>{};
});
package generated_program;

import java.util.Map;

import com.pulumi.Pulumi;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;
import com.pulumi.kubernetes.helm.v4.inputs.RepositoryOptsArgs;
import com.pulumi.asset.FileAsset;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var nginx = new Chart("nginx", ChartArgs.builder()
                    .chart("nginx")
                    .repositoryOpts(RepositoryOptsArgs.builder()
                            .repo("https://charts.bitnami.com/bitnami")
                            .build())
                    .valueYamlFiles(new FileAsset("./values.yaml"))
                    .values(Map.of(
                            "service", Map.of(
                                    "type", "ClusterIP"),
                            "notes", new FileAsset("./notes.txt")))
                    .build());
        });
    }
}
name: example
runtime: yaml
resources:
  nginx:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      chart: nginx
      repositoryOpts:
        repo: https://charts.bitnami.com/bitnami
      valueYamlFiles:
      - fn::fileAsset: values.yaml
      values:
        service:
          type: ClusterIP
        notes:
          fn::fileAsset: notes.txt

The valueYamlFiles property accepts Pulumi FileAssets that are read and merged with the chart’s default values. The values property provides inline overrides with highest precedence. You can use nested maps, Pulumi outputs, and FileAssets as values. Assets are automatically opened and converted to strings.

Deploy a chart into a specific namespace

Kubernetes namespaces isolate resources by environment or team. Charts need explicit namespace configuration to deploy outside the default.

import * as pulumi from "@pulumi/pulumi";
import * as k8s from "@pulumi/kubernetes";

const ns = new k8s.core.v1.Namespace("nginx", {
    metadata: { name: "nginx" },
});
const nginx = new k8s.helm.v4.Chart("nginx", {
    namespace: ns.metadata.name,
    chart: "nginx",
    repositoryOpts: {
        repo: "https://charts.bitnami.com/bitnami",
    }
});
import pulumi
from pulumi_kubernetes.meta.v1 import ObjectMetaArgs
from pulumi_kubernetes.core.v1 import Namespace
from pulumi_kubernetes.helm.v4 import Chart,RepositoryOptsArgs

ns = Namespace("nginx",
    metadata=ObjectMetaArgs(
        name="nginx",
    )
)
nginx = Chart("nginx",
    namespace=ns.metadata.name,
    chart="nginx",
    repository_opts=RepositoryOptsArgs(
        repo="https://charts.bitnami.com/bitnami",
    )
)
package main

import (
	corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
	helmv4 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v4"
	metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
	"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
	pulumi.Run(func(ctx *pulumi.Context) error {
		ns, err := corev1.NewNamespace(ctx, "nginx", &corev1.NamespaceArgs{
			Metadata: &metav1.ObjectMetaArgs{Name: pulumi.String("nginx")},
		})
		if err != nil {
			return err
		}
		_, err = helmv4.NewChart(ctx, "nginx", &helmv4.ChartArgs{
            Namespace: ns.Metadata.Name(),
			Chart:     pulumi.String("nginx"),
			RepositoryOpts: &helmv4.RepositoryOptsArgs{
				Repo: pulumi.String("https://charts.bitnami.com/bitnami"),
			},
		})
		if err != nil {
			return err
		}
		return nil
	})
}
using Pulumi;
using Pulumi.Kubernetes.Types.Inputs.Core.V1;
using Pulumi.Kubernetes.Types.Inputs.Meta.V1;
using Pulumi.Kubernetes.Types.Inputs.Helm.V4;
using System.Collections.Generic;

return await Deployment.RunAsync(() =>
{
    var ns = new Pulumi.Kubernetes.Core.V1.Namespace("nginx", new NamespaceArgs
    {
        Metadata = new ObjectMetaArgs{Name = "nginx"}
    });
    new Pulumi.Kubernetes.Helm.V4.Chart("nginx", new ChartArgs
    {
        Namespace = ns.Metadata.Apply(m => m.Name),
        Chart = "nginx",
        RepositoryOpts = new RepositoryOptsArgs
        {
            Repo = "https://charts.bitnami.com/bitnami"
        },
    });
    
    return new Dictionary<string, object?>{};
});
package generated_program;

import com.pulumi.Pulumi;
import com.pulumi.kubernetes.core.v1.Namespace;
import com.pulumi.kubernetes.core.v1.NamespaceArgs;
import com.pulumi.kubernetes.helm.v4.Chart;
import com.pulumi.kubernetes.helm.v4.ChartArgs;
import com.pulumi.kubernetes.helm.v4.inputs.RepositoryOptsArgs;
import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;
import com.pulumi.core.Output;

public class App {
    public static void main(String[] args) {
        Pulumi.run(ctx -> {
            var ns = new Namespace("nginx", NamespaceArgs.builder()
                    .metadata(ObjectMetaArgs.builder()
                            .name("nginx")
                            .build())
                    .build());
            var nginx = new Chart("nginx", ChartArgs.builder()
                    .namespace(ns.metadata().apply(m -> Output.of(m.name().get())))
                    .chart("nginx")
                    .repositoryOpts(RepositoryOptsArgs.builder()
                            .repo("https://charts.bitnami.com/bitnami")
                            .build())
                    .build());
        });
    }
}
name: example
runtime: yaml
resources:
  ns:
    type: kubernetes:core/v1:Namespace
    properties:
      metadata:
        name: nginx
  nginx:
    type: kubernetes:helm.sh/v4:Chart
    properties:
      namespace: ${ns.metadata.name}
      chart: nginx
      repositoryOpts:
        repo: https://charts.bitnami.com/bitnami

The namespace property controls where chart resources are created. This example creates a Namespace resource first, then references its name when deploying the chart. Pulumi applies a default namespace based on the namespace input, the provider’s configured namespace, and the active Kubernetes context.

Beyond these examples

These snippets focus on specific Chart-level features: chart sources (local, repository, OCI), values customization, and namespace placement. They’re intentionally minimal rather than full application deployments.

The examples assume pre-existing infrastructure such as a Kubernetes cluster with configured kubeconfig, and Helm repositories added to local configuration (for chart references with repo prefix). They focus on configuring the Chart rather than provisioning the cluster or managing Helm repositories.

To keep things focused, common Chart patterns are omitted, including:

  • CRD installation control (skipCrds)
  • Resource readiness waiting (skipAwait)
  • Chart verification and signing (verify, keyring)
  • Dependency management (dependencyUpdate)
  • Post-rendering transformations (postRenderer)
  • Resource name prefixing (resourcePrefix)

These omissions are intentional: the goal is to illustrate how each Chart feature is wired, not provide drop-in Kubernetes modules. See the Helm Chart resource reference for all available configuration options.

Let's deploy Kubernetes Applications with Helm Charts

Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.

Try Pulumi Cloud for FREE

Frequently Asked Questions

Chart Sources & Resolution
How do I specify a Helm chart source?
You can specify a chart using six methods: (1) chart reference with repo prefix (example/mariadb), (2) path to packaged chart (./nginx-1.2.3.tgz), (3) path to unpacked directory (./nginx), (4) absolute URL (https://example.com/charts/nginx-1.2.3.tgz), (5) chart reference with repositoryOpts (chart: "nginx" with repo URL), or (6) OCI registry (oci://example.com/charts/nginx with version).
How do I install a chart from an OCI registry?
Set chart to an OCI URL (e.g., oci://registry-1.docker.io/bitnamicharts/nginx) and specify the version property.
What happens if I don't specify a chart version?
The latest stable version is installed. Use devel: true to include development versions (alpha, beta, release candidates), or specify an exact version.
Values & Configuration
How do I pass values to my chart?
Use valueYamlFiles for values files (as Pulumi Assets) and values for a map of chart values. The values input has highest precedence and supports literals, nested maps, Pulumi outputs, and assets.
Can I use Helm's --set expressions to configure values?
No, expressions like --set service.type aren’t supported. Use the values input with nested maps instead.
How do I deploy a chart to a specific namespace?
Set the namespace property. The namespace is determined by this input, the provider’s configured namespace, and the active Kubernetes context.
Resource Management & Ordering
Does the Chart resource create a Helm Release?
No, Chart renders templates (equivalent to helm template --dry-run=server) and manages resources directly with Pulumi. It doesn’t use Tiller or create a Helm Release.
How does Pulumi determine the order to apply resources?
Pulumi uses heuristics to determine apply and delete order, and waits for each resource to be fully reconciled unless skipAwait is enabled. Use the config.kubernetes.io/depends-on annotation to declare explicit dependencies.
Can I reference resources outside the Chart in dependency annotations?
No, the config.kubernetes.io/depends-on annotation only supports references to resources within the same Chart.
Are CRDs installed by default?
Yes, CRDs are installed if not already present. Set skipCrds: true to skip installing CRDs from the chart’s crds/ directory.
Dependencies & Chart Management
How are chart dependencies handled?
For unpacked chart directories, Pulumi automatically rebuilds dependencies if they’re missing and a Chart.lock file exists. Set dependencyUpdate: true to update dependencies before installation.
Version Selection & Compatibility
Should I use Chart v4 or the Release resource for production?
The v3 Release resource is recommended for production use cases. Chart v4 offers new features and language support, but evaluate the trade-offs for your use case.