Kubernetes

Building a Multi-Namespace Helm Chart with Environment Overlays

Intermediate45 min to complete10 min read

Structure a Helm chart that deploys cleanly to dev, staging, and production with different values per environment — without duplicating templates or maintaining separate charts per namespace.

Before you begin

  • Helm 3 installed
  • kubectl configured with cluster access
  • Basic Helm knowledge (install
  • upgrade
  • template)
Kubernetes
Helm
DevOps
GitOps
Multi-Environment

The typical Helm anti-pattern: one chart for dev, a fork for staging, another fork for prod. They diverge over time. A fix applied to one isn't applied to the others. Three months later, nobody's sure which is canonical.

The right approach: one chart, multiple values files, deployed to separate namespaces. This tutorial builds that structure from scratch.

The Target Structure

charts/api-server/
├── Chart.yaml
├── values.yaml              # Defaults (safe for dev)
├── values-staging.yaml      # Staging overrides
├── values-production.yaml   # Production overrides
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    ├── configmap.yaml
    ├── hpa.yaml
    └── _helpers.tpl

Deploy commands:

bash
1# Dev
2helm upgrade --install api-server ./charts/api-server \
3  -n dev --create-namespace
4
5# Staging
6helm upgrade --install api-server ./charts/api-server \
7  -n staging --create-namespace \
8  -f charts/api-server/values-staging.yaml
9
10# Production
11helm upgrade --install api-server ./charts/api-server \
12  -n production --create-namespace \
13  -f charts/api-server/values-production.yaml

Step 1: Scaffold the Chart

bash
mkdir -p charts/api-server/templates

Step 2: Chart.yaml

bash
1cat > charts/api-server/Chart.yaml <<EOF
2apiVersion: v2
3name: api-server
4description: API server — deployed to dev, staging, and production
5type: application
6version: 0.1.0
7appVersion: "1.0.0"
8EOF

Step 3: Default values.yaml

These are the dev defaults — permissive, low-resource, single replica:

bash
1cat > charts/api-server/values.yaml <<EOF
2replicaCount: 1
3
4image:
5  repository: myregistry/api-server
6  tag: "latest"
7  pullPolicy: Always
8
9service:
10  type: ClusterIP
11  port: 80
12  targetPort: 8080
13
14resources:
15  requests:
16    cpu: 100m
17    memory: 128Mi
18  limits:
19    cpu: 500m
20    memory: 256Mi
21
22autoscaling:
23  enabled: false
24  minReplicas: 1
25  maxReplicas: 5
26  targetCPUUtilizationPercentage: 70
27
28env:
29  LOG_LEVEL: "debug"
30  DATABASE_URL: "postgres://dev-db:5432/appdb"
31
32probes:
33  readiness:
34    path: /healthz
35    initialDelaySeconds: 5
36  liveness:
37    path: /healthz
38    initialDelaySeconds: 15
39
40ingress:
41  enabled: false
42EOF

Step 4: Staging Override Values

bash
1cat > charts/api-server/values-staging.yaml <<EOF
2replicaCount: 2
3
4image:
5  tag: "staging"
6  pullPolicy: IfNotPresent
7
8resources:
9  requests:
10    cpu: 200m
11    memory: 256Mi
12  limits:
13    cpu: 1000m
14    memory: 512Mi
15
16autoscaling:
17  enabled: true
18  minReplicas: 2
19  maxReplicas: 8
20
21env:
22  LOG_LEVEL: "info"
23  DATABASE_URL: "postgres://staging-db:5432/appdb"
24
25ingress:
26  enabled: true
27  host: api.staging.example.com
28EOF

Step 5: Production Override Values

bash
1cat > charts/api-server/values-production.yaml <<EOF
2replicaCount: 3
3
4image:
5  tag: "v1.2.3"           # Always pin in production
6  pullPolicy: IfNotPresent
7
8resources:
9  requests:
10    cpu: 500m
11    memory: 512Mi
12  limits:
13    cpu: 2000m
14    memory: 1Gi
15
16autoscaling:
17  enabled: true
18  minReplicas: 3
19  maxReplicas: 20
20
21env:
22  LOG_LEVEL: "warn"
23  DATABASE_URL: "postgres://prod-db:5432/appdb"
24
25probes:
26  readiness:
27    initialDelaySeconds: 10
28  liveness:
29    initialDelaySeconds: 30
30
31ingress:
32  enabled: true
33  host: api.example.com
34EOF

Step 6: Templates

_helpers.tpl

bash
1cat > charts/api-server/templates/_helpers.tpl <<'EOF'
2{{/*
3Expand the name of the chart.
4*/}}
5{{- define "api-server.name" -}}
6{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
7{{- end }}
8
9{{/*
10Create a default fully qualified app name.
11*/}}
12{{- define "api-server.fullname" -}}
13{{- printf "%s-%s" .Release.Name (include "api-server.name" .) | trunc 63 | trimSuffix "-" }}
14{{- end }}
15
16{{/*
17Common labels
18*/}}
19{{- define "api-server.labels" -}}
20helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
21app.kubernetes.io/name: {{ include "api-server.name" . }}
22app.kubernetes.io/instance: {{ .Release.Name }}
23app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
24app.kubernetes.io/managed-by: {{ .Release.Service }}
25{{- end }}
26EOF

deployment.yaml

bash
1cat > charts/api-server/templates/deployment.yaml <<'EOF'
2apiVersion: apps/v1
3kind: Deployment
4metadata:
5  name: {{ include "api-server.fullname" . }}
6  namespace: {{ .Release.Namespace }}
7  labels:
8    {{- include "api-server.labels" . | nindent 4 }}
9spec:
10  {{- if not .Values.autoscaling.enabled }}
11  replicas: {{ .Values.replicaCount }}
12  {{- end }}
13  selector:
14    matchLabels:
15      app.kubernetes.io/name: {{ include "api-server.name" . }}
16      app.kubernetes.io/instance: {{ .Release.Name }}
17  strategy:
18    type: RollingUpdate
19    rollingUpdate:
20      maxSurge: 1
21      maxUnavailable: 0
22  template:
23    metadata:
24      labels:
25        {{- include "api-server.labels" . | nindent 8 }}
26    spec:
27      terminationGracePeriodSeconds: 60
28      containers:
29        - name: api
30          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
31          imagePullPolicy: {{ .Values.image.pullPolicy }}
32          ports:
33            - containerPort: {{ .Values.service.targetPort }}
34          env:
35            {{- range $key, $val := .Values.env }}
36            - name: {{ $key }}
37              value: {{ $val | quote }}
38            {{- end }}
39          readinessProbe:
40            httpGet:
41              path: {{ .Values.probes.readiness.path }}
42              port: {{ .Values.service.targetPort }}
43            initialDelaySeconds: {{ .Values.probes.readiness.initialDelaySeconds }}
44            periodSeconds: 5
45            failureThreshold: 3
46          livenessProbe:
47            httpGet:
48              path: {{ .Values.probes.liveness.path }}
49              port: {{ .Values.service.targetPort }}
50            initialDelaySeconds: {{ .Values.probes.liveness.initialDelaySeconds }}
51            periodSeconds: 10
52            failureThreshold: 3
53          resources:
54            {{- toYaml .Values.resources | nindent 12 }}
55          lifecycle:
56            preStop:
57              exec:
58                command: ["sleep", "15"]
59EOF

hpa.yaml

bash
1cat > charts/api-server/templates/hpa.yaml <<'EOF'
2{{- if .Values.autoscaling.enabled }}
3apiVersion: autoscaling/v2
4kind: HorizontalPodAutoscaler
5metadata:
6  name: {{ include "api-server.fullname" . }}
7  namespace: {{ .Release.Namespace }}
8spec:
9  scaleTargetRef:
10    apiVersion: apps/v1
11    kind: Deployment
12    name: {{ include "api-server.fullname" . }}
13  minReplicas: {{ .Values.autoscaling.minReplicas }}
14  maxReplicas: {{ .Values.autoscaling.maxReplicas }}
15  metrics:
16    - type: Resource
17      resource:
18        name: cpu
19        target:
20          type: Utilization
21          averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
22{{- end }}
23EOF

Step 7: Validate Before Deploying

bash
1# Render templates without deploying
2helm template api-server ./charts/api-server \
3  -n dev \
4  | kubectl apply --dry-run=client -f -
5
6# Render staging overlay
7helm template api-server ./charts/api-server \
8  -n staging \
9  -f charts/api-server/values-staging.yaml \
10  | kubectl apply --dry-run=client -f -
11
12# Lint
13helm lint ./charts/api-server
14helm lint ./charts/api-server -f charts/api-server/values-production.yaml

Step 8: Deploy to All Environments

bash
1# Dev
2helm upgrade --install api-server ./charts/api-server \
3  --namespace dev --create-namespace \
4  --atomic --timeout 3m
5
6# Staging
7helm upgrade --install api-server ./charts/api-server \
8  --namespace staging --create-namespace \
9  --atomic --timeout 3m \
10  -f charts/api-server/values-staging.yaml
11
12# Production (with explicit version)
13helm upgrade --install api-server ./charts/api-server \
14  --namespace production --create-namespace \
15  --atomic --timeout 5m \
16  -f charts/api-server/values-production.yaml \
17  --set image.tag=v1.2.3

--atomic rolls back automatically if the deployment fails. --timeout sets a deadline for the rollout to complete.

Verify

bash
1# Check releases across namespaces
2helm list -A
3
4# Compare actual values per environment
5helm get values api-server -n dev
6helm get values api-server -n production
7
8# Check running image tags
9kubectl get deployment api-server -n production \
10  -o jsonpath='{.spec.template.spec.containers[0].image}'

The Key Rules

Pin image tags in productionlatest in production is how you get silent breaking changes.

Keep defaults safe for dev — the base values.yaml should be the least dangerous configuration. Production adds constraints, not removes them.

Never commit secrets to values files — use --set at deploy time, external-secrets-operator, or Vault. Values files go in git; secrets don't.

Official References

  • Helm Documentation — Official Helm docs: chart structure, templating, values, hooks, and the Helm CLI
  • Chart Template Guide — In-depth guide to Go templating in Helm, including conditionals, loops, and named templates
  • Helm Best Practices — Official guidance on chart structure, values naming conventions, and labels
  • Helm Hooks — Pre/post install, upgrade, and delete hooks for running jobs at lifecycle events
  • Library Charts — How to share templates across multiple charts to reduce duplication

We built Podscape to simplify Kubernetes workflows like this — logs, events, and cluster state in one interface, without switching tools.

Struggling with this in production?

We help teams fix these exact issues. Our engineers have deployed these patterns across production environments at scale.