---
title: "Helm Charts: Templating & Multi-Environment Kubernetes Deployments"
description: "How Helm templates Kubernetes manifests for multi-environment deployments: values overrides per environment, conditional logic, chart dependencies, and GitOps rollout with ArgoCD."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/devtips/post/helm-charts-kubernetes-multi-environment
---

# Helm Charts: Templating & Multi-Environment Kubernetes Deployments

## Why Helm matters

### The Kubernetes manifest problem

**Managing Kubernetes manifests at scale becomes a nightmare.**

You have a deployment for dev, staging and production. Each one is 90% identical: same containers, same services, different replicas and resource limits and environment variables. Copy the YAML, edit a few lines, deploy. That works right up until someone edits dev and forgets staging, or a typo lands in production and nobody notices until users report the outage.

Helm packages a Kubernetes application as a chart with templates, values and a version number. The YAML you deploy is generated from one source instead of maintained in three places.

### Where hand-written YAML breaks down

```
Environment Fragmentation:
prod-deployment.yaml  ← Different values hardcoded
staging-deployment.yaml  ← Needs 3 edits for prod
dev-deployment.yaml  ← Inconsistent structure

Result:
• Configurations drift
• Changes in one place aren't replicated
• Rollback = manually revert files
• New environments = copy-paste hell
```

## The fix: Helm charts

### What Helm is

**Helm is package management for Kubernetes**, like npm for Node.js or pip for Python.

- **Charts**: Helm packages containing templated Kubernetes manifests
- **Values**: Configuration that gets injected into templates (replicas, image tags, resources)
- **Releases**: Deployed instances of charts, tracked with versions for easy rollback
- **Repos**: Central repositories where teams share charts

### What you get

- **Single chart, multiple environments**: Use template variables instead of duplicating YAML
- **Templating system**: `{{ .Values.replicas }}` → substituted with values from env-specific file
- **Dependency management**: Charts can depend on other charts (PostgreSQL, Redis, etc.)
- **Versioning & rollback**: Every deployment tracked, instant rollback to previous version
- **Validation**: Helm validates charts before deployment
- **GitOps ready**: Store charts in Git, deploy from Git

## Chart structure

### Directory layout

```
my-app/
├── Chart.yaml                 # Chart metadata (name, version, description)
├── values.yaml                # Default values
├── values-dev.yaml            # Dev environment overrides
├── values-staging.yaml        # Staging overrides
├── values-prod.yaml           # Production overrides
├── templates/
│   ├── deployment.yaml        # Templated Kubernetes Deployment
│   ├── service.yaml           # Service manifest
│   ├── configmap.yaml         # ConfigMap for config
│   ├── hpa.yaml               # Horizontal Pod Autoscaler (prod only)
│   └── _helpers.tpl           # Template helpers/functions
└── README.md                  # Chart documentation
```

### Chart.yaml

```yaml title="Chart.yaml"
apiVersion: v2
name: my-app
description: A Helm chart for my microservice
type: application
version: 1.0.0 # Chart version
appVersion: '1.2.3' # Application version
maintainers:
  - name: Your Team
    email: team@example.com
```

## Templating

### Basic values templating

```yaml title="values.yaml"
# Default values for all environments
replicaCount: 1

image:
  repository: myregistry.azurecr.io/my-app
  tag: '1.2.3'
  pullPolicy: IfNotPresent

resources:
  requests:
    memory: '128Mi'
    cpu: '100m'
  limits:
    memory: '256Mi'
    cpu: '500m'

environment: 'dev'
debug: true
```

```yaml title="values-prod.yaml"
# Production overrides
replicaCount: 3 # More replicas for load

resources:
  requests:
    memory: '512Mi'
    cpu: '500m'
  limits:
    memory: '1Gi'
    cpu: '2000m'

environment: 'production'
debug: false # Disable debug logging
```

### A templated deployment

```yaml title="templates/deployment.yaml"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{.Release.Name}}
  namespace: {{.Release.Namespace}}
spec:
  replicas: {{.Values.replicaCount}} # Injected from values
  selector:
    matchLabels:
      app: {{.Chart.Name}}
  template:
    metadata:
      labels:
        app: {{.Chart.Name}}
    spec:
      containers:
        - name: {{.Chart.Name}}
          image: '{{ .Values.image.repository }}:{{ .Values.image.tag }}'
          imagePullPolicy: {{.Values.image.pullPolicy}}
          env:
            - name: ENVIRONMENT
              value: {{.Values.environment}}
            - name: DEBUG
              value: '{{ .Values.debug }}'
          ports:
            - containerPort: 8080
          resources:
            requests:
              memory: {{.Values.resources.requests.memory | quote}}
              cpu: {{.Values.resources.requests.cpu | quote}}
            limits:
              memory: {{.Values.resources.limits.memory | quote}}
              cpu: {{.Values.resources.limits.cpu | quote}}
```

## Conditional logic in templates

### Environment-specific configuration

```yaml title="templates/deployment.yaml"
spec:
  {{- if eq .Values.environment "production" }}
  replicas: 3
  {{- else if eq .Values.environment "staging" }}
  replicas: 2
  {{- else }}
  replicas: 1
  {{- end }}
```

### Resources that exist only in some environments

```yaml title="templates/hpa.yaml"
{{- if eq .Values.environment "production" }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: {{ .Release.Name }}
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: {{ .Release.Name }}
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
{{- end }}
```

### Conditional security settings

```yaml title="templates/deployment.yaml"
spec:
  {{- if eq .Values.environment "production" }}
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsReadOnlyRootFilesystem: true
  {{- end }}
```

## Managing multiple environments

### Environment-specific values files

```
Deploy to dev:
helm install my-app . -f values.yaml -f values-dev.yaml

Deploy to staging:
helm install my-app . -f values.yaml -f values-staging.yaml

Deploy to prod:
helm install my-app . -f values.yaml -f values-prod.yaml
```

### Values priority (the last one wins)

```bash
# Later files override earlier ones
helm install my-app \
  -f values.yaml \           # Base defaults
  -f values-prod.yaml \      # Override for prod
  --set environment=production # Override from CLI (highest priority)
```

## Chart dependencies

### Depending on PostgreSQL

```yaml title="Chart.yaml"
dependencies:
  - name: postgresql
    version: '13.0.0'
    repository: https://charts.bitnami.com/bitnami
```

### Installing dependencies

```bash
# Download dependencies
helm dependency update

# Then deploy (PostgreSQL chart auto-installs)
helm install my-app . -f values-prod.yaml
```

### Values for a sub-chart

```yaml title="values-prod.yaml"
# My app values
replicaCount: 3

# PostgreSQL sub-chart values
postgresql:
  enabled: true
  auth:
    password: 'prod-secure-password'
  primary:
    persistence:
      size: 100Gi
  metrics:
    enabled: true
```

## Release management

### The basic commands

```bash
# Install a release
helm install my-app ./chart -f values-prod.yaml

# Upgrade to new version
helm upgrade my-app ./chart -f values-prod.yaml

# Check release history
helm history my-app

# Rollback to previous version (instant!)
helm rollback my-app 2

# Delete release
helm uninstall my-app
```

### A deployment run, start to finish

```bash
# 1. Make changes to chart
# 2. Test locally
helm install test-release . -f values-dev.yaml

# 3. Test succeeded, upgrade
helm uninstall test-release

# 4. Deploy to production
helm upgrade my-app . -f values-prod.yaml

# 5. Verify
kubectl get pods -l app=my-app

# 6. If something's wrong, instant rollback
helm rollback my-app
```

## GitOps integration

### Storing charts in Git

```
git repository structure:
├── charts/
│   ├── my-app/
│   │   ├── Chart.yaml
│   │   ├── values.yaml
│   │   ├── values-dev.yaml
│   │   ├── values-prod.yaml
│   │   └── templates/
│   └── other-app/
├── .gitignore
└── README.md
```

### GitOps with ArgoCD

ArgoCD watches your Git repo and automatically keeps your Kubernetes cluster in sync:

```yaml title="argocd-app.yaml"
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-prod
spec:
  project: default
  source:
    repoURL: https://github.com/myteam/infrastructure
    targetRevision: main
    path: charts/my-app
    helm:
      values: |
        environment: production
        replicaCount: 3
  destination:
    server: https://kubernetes.default.svc
    namespace: prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
```

Deploy workflow:

```
1. Developer updates Chart in Git
2. Commits and pushes
3. ArgoCD detects change
4. Automatically syncs to Kubernetes
5. Helm applies new deployment
6. Services updated with zero downtime
```

## Going further with templates

### Helper functions

```yaml title="templates/_helpers.tpl"
{{- define "my-app.labels" -}}
helm.sh/chart: {{ include "my-app.chart" . }}
app.kubernetes.io/name: {{ include "my-app.name" . }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}

{{- define "my-app.name" -}}
{{ .Chart.Name }}
{{- end }}
```

```yaml title="templates/deployment.yaml"
# Reuse helper
metadata:
  labels: {{- include "my-app.labels" . | nindent 4}}
```

### Loops

```yaml title="templates/deployment.yaml"
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
  value: {{ $value | quote }}
{{- end }}
```

## Best practices

### 1. Version charts semantically

```yaml # Chart.yaml
version: 2.1.0 # MAJOR.MINOR.PATCH
# MAJOR: Breaking changes
# MINOR: New features
# PATCH: Bug fixes
```

### 2. Use namespaces

```bash
# Deploy each environment to different namespace
helm install app . -n production -f values-prod.yaml
helm install app . -n staging -f values-staging.yaml
helm install app . -n dev -f values-dev.yaml
```

### 3. Lint before you deploy

```bash
# Validate chart syntax
helm lint ./chart

# Dry-run to see what will be deployed
helm install --dry-run my-app ./chart -f values-prod.yaml
```

### 4. Test charts

```bash
helm test my-app  # Run chart tests
```

### 5. Validate values against a schema

```yaml title="values.schema.json"
{
  '$schema': 'https://json-schema.org/draft-07/schema#',
  'type': 'object',
  'properties':
    {
      'replicaCount': {'type': 'integer', 'minimum': 1},
      'image': {'type': 'object', 'properties': {'tag': {'type': 'string'}}},
    },
}
```

## A complete minimal example

### Chart structure

```
my-service/
├── Chart.yaml
├── values.yaml
├── values-prod.yaml
└── templates/
    ├── deployment.yaml
    └── service.yaml
```

### Deploying it

```bash
# Dev
helm install my-service . -f values.yaml -f values-dev.yaml

# Production
helm upgrade my-service . -f values.yaml -f values-prod.yaml --install

# Rollback if needed
helm rollback my-service
```

## Wrapping up

**Helm replaces four copies of a deployment with one chart and four values files.**

Templated manifests, values per environment and numbered releases mean a rollback is one command instead of an archaeology session in Git history. Pair it with ArgoCD and the cluster follows the repo. The templating language is the price of admission, and it is a real cost, but it buys you environments that actually match.

## Resources

- [Helm Official Documentation](https://helm.sh/docs/)
- [Chart Template Guide](https://helm.sh/docs/chart_template_guide/)
- [Helm Best Practices](https://helm.sh/docs/chart_best_practices/)
- [ArtifactHub: Pre-built Charts](https://artifacthub.io/)
- [ArgoCD + Helm Integration](https://argo-cd.readthedocs.io/en/stable/user-guide/helm/)
