Helm charts can feel like a black box when it comes to how they handle configuration, especially when you’re trying to ensure certain values are always provided or have sensible defaults.

Let’s see what happens when we deploy a simple chart with a missing required value and then how we set up defaults.

First, imagine a basic values.yaml in our chart:

# values.yaml
replicaCount: 1
image:
  repository: nginx
  tag: latest
service:
  type: ClusterIP
  port: 80
database:
  enabled: false

And a Chart.yaml:

# Chart.yaml
apiVersion: v2
name: my-nginx
version: 0.1.0
description: A simple Nginx chart

Now, let’s say we want to make image.tag mandatory. We’ll put that logic in templates/NOTES.txt to illustrate a common pattern, though it’s often handled in _helpers.tpl or directly in the templates.

# templates/NOTES.txt

{{- if not .Values.image.tag -}}

Error: image.tag is required and cannot be empty. Please set it in your values.yaml or via --set image.tag=<your-tag>.

{{- end -}}


# ... rest of your notes ...

If we try to install this chart without image.tag defined:

helm install my-release ./my-nginx-chart

Helm will render NOTES.txt and show us the error:

Error: image.tag is required and cannot be empty. Please set it in your values.yaml or via --set image.tag=<your-tag>.

This NOTES.txt check is a runtime validation. Helm itself doesn’t have a built-in "required" flag for values. You achieve this by checking for the value’s existence and failing the render or installation if it’s not there.

Now, let’s talk about default values. These are crucial for making charts user-friendly and preventing the exact kind of errors we just saw.

Consider a more robust values.yaml:

# values.yaml
replicaCount: 1
image:
  repository: nginx
  # Default tag if not provided
  tag: "1.21.6"
service:
  type: ClusterIP
  port: 80
database:
  enabled: false
  host: "localhost"
  port: 5432

And in our templates/deployment.yaml, we reference these values:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:

  name: {{ include "my-nginx.fullname" . }}

  labels:

    {{- include "my-nginx.labels" . | nindent 4 }}

spec:

  replicas: {{ .Values.replicaCount }}

  selector:
    matchLabels:

      {{- include "my-nginx.selectorLabels" . | nindent 6 }}

  template:
    metadata:
      labels:

        {{- include "my-nginx.selectorLabels" . | nindent 8 }}

    spec:
      containers:

        - name: {{ .Chart.Name }}


          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"

          ports:

            - containerPort: {{ .Values.service.port }}

          env:
            - name: DB_HOST

              value: {{ .Values.database.host | quote }}

            - name: DB_PORT

              value: {{ .Values.database.port | quote }}

Notice the line: image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}".

The | default .Chart.AppVersion part is key. If values.yaml doesn’t provide image.tag, Helm will use the AppVersion defined in Chart.yaml as the default. If image.tag is provided in values.yaml (or via --set), that value will be used instead.

Let’s assume our Chart.yaml has:

# Chart.yaml
apiVersion: v2
name: my-nginx
version: 0.1.0
appVersion: "1.21.6" # This is used as a default if image.tag is missing
description: A simple Nginx chart

If we run helm install my-release ./my-nginx-chart with just the values.yaml above (which does have image.tag: "1.21.6"), the deployment will use nginx:1.21.6.

If we remove image.tag from values.yaml and run the same install command, the | default .Chart.AppVersion will kick in, and the image will be nginx:1.21.6 (because appVersion in Chart.yaml is "1.21.6").

If we want to override the default tag, we can use --set:

helm install my-release ./my-nginx-chart --set image.tag="latest"

This will result in the image nginx:latest being used.

The default function is a Go template function provided by Helm. It takes two arguments: the value to check, and the default value to use if the first value is empty or not set. This is a very common and powerful way to make charts flexible.

You can chain these or use them with other template functions. For example, if you wanted to ensure a database.host was always set, but had a default if not provided:

# templates/statefulset.yaml (example for a database)
# ...
env:
  - name: POSTGRES_HOST

    value: {{ .Values.database.host | default "postgres.local" | quote }}

  - name: POSTGRES_PORT

    value: {{ .Values.database.port | default 5432 | quote }}

# ...

If values.yaml has database.host: "my-db.prod.local", the environment variable will be POSTGRES_HOST="my-db.prod.local". If database.host is missing, it will be POSTGRES_HOST="postgres.local".

The most surprising true thing about Helm’s value handling is that there’s no explicit "required" declaration in values.yaml itself. You must implement validation logic, typically in your templates or NOTES.txt, to enforce mandatory values.

Here’s how a database host might be configured in values.yaml:

# values.yaml
database:
  enabled: true
  host: "db.example.com"
  port: 5432
  user: "admin"
  password: "supersecretpassword" # In a real chart, this would be handled via secrets!

And the corresponding template snippet to render a connection string:

# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:

  name: {{ include "my-app.fullname" . }}-config

data:

  DATABASE_URL: "postgresql://{{ .Values.database.user }}:{{ .Values.database.password }}@{{ .Values.database.host }}:{{ .Values.database.port }}/myappdb"

If you were to deploy this and values.yaml was missing database.host, the DATABASE_URL would try to render with an empty host, likely causing a Kubernetes Pod to fail.

The default function is your primary tool for providing sensible fallback values when a user omits a configuration option. It’s a simple but incredibly effective way to make your charts more resilient and easier to use out-of-the-box.

The next thing you’ll likely run into is managing sensitive values like passwords and API keys, which should never be stored directly in values.yaml.

Want structured learning?

Take the full Helm course →