Helm Advanced Patterns: Chart Development and Production Operations
Helm is Kubernetes' package manager — but production Helm usage goes well beyond helm install. This covers the patterns that matter at scale: library charts for shared templates, chart hooks for database migrations, OCI registry publishing, values schema validation, and the Helmfile pattern for managing multiple releases as code.

Most teams use Helm as a deployment tool — helm install, helm upgrade, helm rollback. Production Helm usage requires more: custom charts that DRY up repeated Kubernetes patterns across services, values schema validation that catches configuration errors before deployment, and hooks that orchestrate database migrations without manual intervention.
Library Charts: Shared Templates Across Services
A library chart contains named templates that other charts can include. It can't be deployed on its own — its purpose is to be a dependency:
my-org-lib/ # Library chart
├── Chart.yaml
└── templates/
├── _deployment.tpl # Shared deployment template
├── _service.tpl
├── _hpa.tpl
└── _helpers.tpl # Common label/annotation helpers
# my-org-lib/Chart.yaml
apiVersion: v2
name: my-org-lib
description: Shared Helm templates for My Org services
type: library # Key: marks this as a library chart
version: 1.2.01# my-org-lib/templates/_deployment.tpl
2{{- define "myorg.deployment" -}}
3apiVersion: apps/v1
4kind: Deployment
5metadata:
6 name: {{ .Release.Name }}
7 labels:
8 {{- include "myorg.labels" . | nindent 4 }}
9spec:
10 replicas: {{ .Values.replicaCount | default 2 }} # Note: default treats 0 as falsy — replicaCount: 0 will still render as 2
11 selector:
12 matchLabels:
13 app: {{ .Release.Name }}
14 template:
15 metadata:
16 labels:
17 app: {{ .Release.Name }}
18 {{- include "myorg.labels" . | nindent 8 }}
19 spec:
20 containers:
21 - name: {{ .Release.Name }}
22 image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
23 ports:
24 - containerPort: {{ .Values.service.port | default 8080 }}
25 resources:
26 {{- toYaml .Values.resources | nindent 12 }}
27 livenessProbe:
28 httpGet:
29 path: {{ .Values.healthCheck.path | default "/health" }}
30 port: {{ .Values.service.port | default 8080 }}
31 initialDelaySeconds: 30
32 periodSeconds: 30
33 readinessProbe:
34 httpGet:
35 path: {{ .Values.healthCheck.path | default "/health" }}
36 port: {{ .Values.service.port | default 8080 }}
37 initialDelaySeconds: 10
38 periodSeconds: 10
39{{- end -}}Using the library chart in an application chart:
1# payments-api/Chart.yaml
2apiVersion: v2
3name: payments-api
4description: Payments API service
5version: 2.1.0
6dependencies:
7 - name: my-org-lib
8 version: "1.2.0"
9 repository: "oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts"# payments-api/templates/deployment.yaml
{{- include "myorg.deployment" . }}# payments-api/templates/service.yaml
{{- include "myorg.service" . }}The application chart's templates/ contains only include calls to the library. All boilerplate lives in one place.
Values Schema Validation
Helm supports JSON Schema validation for values files. This catches misconfiguration before Helm attempts to render templates:
1// values.schema.json
2{
3 "$schema": "http://json-schema.org/draft-07/schema#",
4 "type": "object",
5 "required": ["image", "service", "resources"],
6 "properties": {
7 "image": {
8 "type": "object",
9 "required": ["repository", "tag"],
10 "properties": {
11 "repository": {
12 "type": "string",
13 "description": "Container image repository"
14 },
15 "tag": {
16 "type": "string",
17 "description": "Image tag or digest",
18 "pattern": "^[a-zA-Z0-9._:-]+$"
19 }
20 }
21 },
22 "service": {
23 "type": "object",
24 "properties": {
25 "port": {
26 "type": "integer",
27 "minimum": 1,
28 "maximum": 65535,
29 "default": 8080
30 }
31 }
32 },
33 "resources": {
34 "type": "object",
35 "required": ["requests"],
36 "properties": {
37 "requests": {
38 "type": "object",
39 "required": ["cpu", "memory"],
40 "properties": {
41 "cpu": {"type": "string"},
42 "memory": {"type": "string"}
43 }
44 }
45 }
46 },
47 "replicaCount": {
48 "type": "integer",
49 "minimum": 1,
50 "maximum": 20,
51 "default": 2
52 }
53 }
54}With values.schema.json in the chart root, helm install and helm upgrade validate values before rendering. A missing required field or wrong type produces a clear error message instead of a cryptic template error.
Note: Helm's JSON Schema validator uses JSON Schema draft-07 regardless of the $schema URI declared in the file. Draft 2020-12 features (like $defs or unevaluatedProperties) are not supported and will silently pass validation.
Chart Hooks: Database Migrations
Hooks run Jobs at specific points in the release lifecycle:
1# templates/pre-upgrade-migration.yaml
2apiVersion: batch/v1
3kind: Job
4metadata:
5 name: {{ .Release.Name }}-pre-upgrade-migration
6 labels:
7 {{- include "myorg.labels" . | nindent 4 }}
8 annotations:
9 "helm.sh/hook": pre-upgrade # Run before upgrade
10 "helm.sh/hook-weight": "-5" # Lower = runs first (multiple hooks)
11 "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
12 # before-hook-creation: delete previous run before creating new
13 # hook-succeeded: delete after successful completion
14spec:
15 backoffLimit: 0 # Don't retry on failure — let the upgrade fail
16 template:
17 spec:
18 restartPolicy: Never
19 containers:
20 - name: migration
21 image: {{ .Values.image.repository }}:{{ .Values.image.tag }}
22 command: ["/app/migrate", "--up", "--verbose"]
23 env:
24 - name: DATABASE_URL
25 valueFrom:
26 secretKeyRef:
27 name: {{ .Release.Name }}-db-credentials
28 key: urlHook phases: pre-install, post-install, pre-upgrade, post-upgrade, pre-rollback, post-rollback, pre-delete, post-delete, test.
If a pre-upgrade hook fails, Helm aborts the upgrade and the current release remains active.
OCI Registry Publishing
OCI chart support was introduced in Helm v3.8 behind HELM_EXPERIMENTAL_OCI=1 and became stable (GA) in Helm v3.12. Helm v3.12+ supports OCI as a chart registry without any feature flag (instead of the legacy helm repo add approach):
1# Push chart to ECR
2aws ecr create-repository --repository-name helm-charts/payments-api
3
4# Log in to ECR
5aws ecr get-login-password --region us-east-1 | \
6 helm registry login \
7 --username AWS \
8 --password-stdin \
9 123456789.dkr.ecr.us-east-1.amazonaws.com
10
11# Package and push
12helm package payments-api/ # Creates payments-api-2.1.0.tgz
13
14helm push payments-api-2.1.0.tgz \
15 oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts
16
17# Pull and install from OCI
18helm install payments-api \
19 oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts/payments-api \
20 --version 2.1.0 \
21 --namespace payments \
22 --values production-values.yamlOCI registries eliminate the need for a separate chart repository server. ECR, GHCR, and Docker Hub all support OCI artifact storage.
Last-Mile Patching: Helm Post-Renderers
In 2026, for third-party charts that don't expose enough knobs in values.yaml, we use Post-Renderers. This allows you to pipe Helm's output through Kustomize (or any binary) to apply patches before the final manifest is sent to Kubernetes:
# A simple post-render script (kustomize-wrapper.sh)
#!/bin/bash
cat <&0 > all.yaml
kustomize build .# Install with the post-renderer
helm install my-app bitnami/nginx \
--post-renderer ./kustomize-wrapper.shThis pattern allows you to inject sidecars, add labels, or modify resources in upstream charts without forking them. It's the "cleanest" way to enforce platform standards on third-party software.
Helmfile: Multi-Release Management as Code
Helmfile manages multiple Helm releases as a single declarative file — similar to how Terraform manages infrastructure:
1# helmfile.yaml
2repositories:
3 - name: ingress-nginx
4 url: https://kubernetes.github.io/ingress-nginx
5 - name: jetstack
6 url: https://charts.jetstack.io
7
8releases:
9 - name: ingress-nginx
10 namespace: ingress-nginx
11 chart: ingress-nginx/ingress-nginx
12 version: 4.11.3
13 values:
14 - infra/nginx-values.yaml
15
16 - name: cert-manager
17 namespace: cert-manager
18 chart: jetstack/cert-manager
19 version: v1.16.2
20 set:
21 - name: crds.enabled
22 value: "true"
23
24 - name: payments-api
25 namespace: payments
26 chart: oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts/payments-api
27 version: 2.1.0
28 values:
29 - apps/payments/values.yaml
30 - apps/payments/values-production.yaml
31 needs:
32 - cert-manager/cert-manager # Ensure cert-manager is installed first1# Apply all releases
2helmfile sync
3
4# Diff what would change
5helmfile diff
6
7# Apply only releases that changed
8helmfile apply
9
10# Target a specific release
11helmfile -l name=payments-api syncHelm with Argo CD
Argo CD renders Helm charts server-side and tracks their output. For a split-repo GitOps setup (chart in OCI, values in a Git repo), use Argo CD's multiple sources feature (GA in v2.8+):
1apiVersion: argoproj.io/v1alpha1
2kind: Application
3metadata:
4 name: payments-api
5 namespace: argocd
6spec:
7 sources:
8 # Source 1: The Helm chart from OCI registry
9 - repoURL: oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts
10 chart: payments-api
11 targetRevision: "2.1.0"
12 helm:
13 releaseName: payments-api
14 valueFiles:
15 - $values/apps/payments/values-prod.yaml # $values references the second source
16
17 # Source 2: Values files from a separate Git config repo
18 - repoURL: https://github.com/my-org/platform-config.git
19 targetRevision: main
20 ref: values # Defines the $values variable used aboveFor single-source deployments (chart and values in the same repo), valueFiles accepts repo-relative paths:
1spec:
2 source:
3 repoURL: https://github.com/my-org/charts.git
4 path: payments-api
5 targetRevision: main
6 helm:
7 releaseName: payments-api
8 valueFiles:
9 - values.yaml # Relative to the chart path
10 - ../../config/values-prod.yaml # Traverse up to a config directory
11 values: |
12 # Inline override (highest precedence)
13 replicaCount: 3Note: valueFiles only accepts repo-relative paths — not HTTPS URLs. To pull values from a separate repository, use multiple sources as shown above.
Argo CD's Helm integration diffs the rendered manifests (not the chart itself) — it shows exactly which Kubernetes resources would change, not which chart values changed.
Frequently Asked Questions
Should I write custom charts or use upstream charts with values?
Upstream charts (bitnami, cert-manager, etc.) for infrastructure components — they're well-maintained and cover most cases. Custom charts for your application services — upstream charts can't know your organization's conventions, labels, or library templates. Library charts let you maintain DRY templates without forking upstream charts.
How do I handle different environments (dev, staging, prod) with Helm?
Multiple values files layered at install time. Base values in values.yaml (defaults that work everywhere), environment-specific overrides in values-prod.yaml. Install with --values values.yaml --values values-prod.yaml (later files override earlier). For Argo CD, use valueFiles in the Application spec to pull environment-specific values from a separate config repo.
Can Helm manage CRDs safely?
Helm's CRD handling has quirks. CRDs in crds/ are installed before templates but never updated or deleted by Helm. This is intentional — CRD changes can break existing custom resources. For CRDs that change frequently (like cert-manager's), use the crds.enabled: true value that installs CRDs as regular resources (upgradeable but deletable). For operator CRDs (Prometheus, etc.), use the operator's own CRD management tooling rather than relying on Helm.
For Argo CD GitOps workflows that deploy Helm charts across environments, see Argo CD for GitOps-Based Kubernetes Deployments. For Helm best practices for production deployments (chart structure, testing, CI), see Helm Best Practices for Production Kubernetes.
Building a Helm library for multiple application teams on a shared platform? Talk to us at Coding Protocols — we help platform teams design chart architectures that standardize deployments without creating a maintenance bottleneck.


