Helm Values and Overrides#

Every Helm chart has a values.yaml file that defines defaults. When you install or upgrade a release, you override those defaults through values files (-f) and inline flags (--set). Getting the precedence wrong leads to silent misconfigurations where you think you set something but the chart used a different value.

Inspecting Chart Defaults#

Before overriding anything, look at what the chart provides. helm show values dumps the full default values.yaml for any chart:

# From a repo
helm show values bitnami/postgresql

# From a local chart directory
helm show values ./my-chart

# From an OCI registry
helm show values oci://registry-1.docker.io/bitnamicharts/postgresql

# Pipe to a file for reference
helm show values bitnami/postgresql > postgresql-defaults.yaml

This is the single most important debugging command. When something behaves unexpectedly, compare your overrides against these defaults.

Override Precedence#

Helm merges values in a specific order. Later sources override earlier ones:

  1. Chart’s values.yaml (lowest priority)
  2. Parent chart’s values.yaml (if this is a subchart)
  3. Values files passed with -f / --values (left to right)
  4. Inline values with --set, --set-string, --set-file, --set-json (highest priority)

The critical rule: rightmost wins. If you pass multiple -f flags, the last file takes precedence for any conflicting keys:

# base.yaml sets replicas: 1
# production.yaml sets replicas: 3
helm upgrade --install my-app ./chart \
  -f values/base.yaml \
  -f values/production.yaml
# Result: replicas = 3 (production.yaml wins)

And --set always beats -f, regardless of ordering:

helm upgrade --install my-app ./chart \
  --set replicaCount=5 \
  -f values/production.yaml
# Result: replicas = 5 (--set wins over -f, always)

–set vs -f: When to Use Which#

Use -f for structured, version-controlled configuration. Use --set for one-off overrides, CI/CD pipeline variables, and secrets you do not want in files.

# -f for environment config (committed to git)
helm upgrade --install my-app ./chart -f values/base.yaml -f values/staging.yaml

# --set for dynamic values from CI/CD
helm upgrade --install my-app ./chart -f values/production.yaml --set image.tag="${GIT_SHA}"

# --set-string forces string type (important for numeric-looking values)
helm upgrade --install my-app ./chart --set-string podAnnotations."prometheus\.io/port"="9090"

# --set-json for complex structures inline
helm upgrade --install my-app ./chart --set-json 'resources={"limits":{"cpu":"500m","memory":"256Mi"}}'

Watch the escaping. Dots navigate nested keys (ingress.hosts[0].host=example.com). Escape literal dots with backslash (nodeSelector."kubernetes\.io/arch"=arm64). Avoid comma-separated --set a=1,b=2 – use separate --set flags instead.

Dry-Run and Template Rendering#

Before applying changes, render templates locally to see the actual manifests Helm will produce:

# helm template: renders locally, no cluster connection needed
helm template my-release ./chart -f values/production.yaml

# helm template with a specific template file
helm template my-release ./chart -s templates/deployment.yaml

# helm upgrade --dry-run: renders against the cluster (validates API versions, checks existing resources)
helm upgrade --install my-app ./chart \
  -f values/production.yaml \
  --dry-run

The difference matters. helm template works offline but cannot validate against your cluster’s API capabilities. --dry-run contacts the cluster, so it catches issues like missing CRDs or unsupported API versions.

Pipe the output to search for specific values to confirm your overrides took effect:

helm template my-release ./chart -f values/production.yaml | grep -A5 "resources:"

Values Schema Validation#

Charts can include a values.schema.json file that validates values before rendering. If the schema exists, Helm rejects invalid values at install/upgrade time:

# This will fail if replicaCount must be an integer and you pass a string
helm upgrade --install my-app ./chart --set replicaCount=abc
# Error: values don't meet the specifications of the schema

When writing your own charts, add a values.schema.json to catch misconfigurations early. Even a minimal schema that enforces required fields and types saves debugging time.

Environment-Specific Values Pattern#

The most common pattern is a layered file structure with base defaults and per-environment overrides:

my-chart/
  values/
    base.yaml          # shared defaults
    dev.yaml            # dev overrides (low resources, debug logging)
    staging.yaml        # staging (moderate resources, staging URLs)
    production.yaml     # production (high resources, real URLs, replicas)
# Dev deployment
helm upgrade --install my-app ./chart -f values/base.yaml -f values/dev.yaml

# Production deployment
helm upgrade --install my-app ./chart -f values/base.yaml -f values/production.yaml

Keep base.yaml as the source of truth for structure, and environment files as minimal diffs. Do not duplicate the entire values file per environment – only override what changes.

Helm OCI Registry Support#

Helm 3.8+ supports OCI registries as chart repositories. This replaces the older helm repo add workflow:

# Pull a chart from an OCI registry
helm pull oci://registry-1.docker.io/bitnamicharts/postgresql --version 16.4.1

# Install directly from OCI
helm upgrade --install my-pg oci://registry-1.docker.io/bitnamicharts/postgresql \
  --version 16.4.1 \
  -f values/postgresql.yaml

# Push your own chart to an OCI registry
helm package ./my-chart
helm push my-chart-1.0.0.tgz oci://ghcr.io/myorg/charts

# Login to a private OCI registry
helm registry login ghcr.io -u USERNAME -p TOKEN

OCI charts do not need helm repo update. Each pull fetches the exact version. This makes builds more reproducible and eliminates stale repo index issues.

Debugging Values Problems#

When a deployment does not behave as expected:

# See what values were explicitly set for a release
helm get values my-release -n my-namespace

# See everything, including chart defaults
helm get values my-release -n my-namespace --all

# See the rendered manifests that were applied
helm get manifest my-release -n my-namespace

# Diff between two revisions (requires helm-diff plugin)
helm diff revision my-release 3 4

Compare the output of helm get values --all against helm show values <chart> to spot unintended defaults overriding your configuration.