Helm Chart Files & Values Reference
A production chart usually starts with Chart.yaml, values.yaml, helpers, Deployment or StatefulSet templates, Service, config, ingress, autoscaling, disruption budget, and security controls. Do not create every file by habit; add a template when the workload or platform contract needs it.
Chart Root Files
These files live at the chart root. They define chart metadata, default configuration, dependency locks, packaged subcharts, CRDs, and files excluded from chart packages.
web-api/
Chart.yaml # Chart metadata, type, version, dependencies.
Chart.lock # Locked dependency versions after helm dependency update.
values.yaml # Default chart API; safe non-secret defaults.
values-dev.yaml # Local/dev overrides.
values-prod.yaml # Production sizing, ingress, PDB, resources.
README.md # Human usage notes and supported values.
.helmignore # Files excluded from packaged chart.
charts/ # Vendored dependencies.
crds/ # CRDs installed before templates.
templates/ # Templated Kubernetes objects.apiVersion: v2
name: web-api
description: Web API application chart
type: application
version: 1.2.0
appVersion: "2.4.7"
dependencies:
- name: redis
version: "19.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabledTemplate Inventory
This is the normal menu of files to consider inside templates/. A small HTTP service may only need Deployment, Service, helpers, and optional Ingress. Stateful apps and platform charts usually need more.
| Template | Purpose | Add when... |
|---|---|---|
_helpers.tpl | Reusable names, labels, selectors, and annotation helpers. | Always, for consistent metadata. |
deployment.yaml | Stateless application Pods managed by a Deployment. | API, web, worker, consumer, frontend services. |
service.yaml | Stable DNS and virtual IP for Pods. | Pods receive traffic from other Pods or ingress. |
serviceaccount.yaml | Dedicated Pod identity. | Pods need RBAC, IRSA, Workload Identity, or non-default identity. |
configmap.yaml | Non-sensitive config, feature flags, app files. | Config should change independently from image. |
secret.yaml | Kubernetes Secret object. | Only for generated or injected secrets; avoid raw Git secrets. |
ingress.yaml | HTTP routing from outside the cluster. | App needs hostname, TLS, or path routing. |
hpa.yaml | Horizontal scaling from metrics. | Workload can scale horizontally and metrics exist. |
pdb.yaml | Minimum availability during voluntary disruptions. | Replicas > 1 and node drains should preserve availability. |
networkpolicy.yaml | Pod ingress/egress rules. | CNI enforces NetworkPolicy and workload traffic is known. |
statefulset.yaml | Stable identity and ordered rollout for stateful Pods. | Databases, queues, clustered systems. |
pvc.yaml | Persistent storage claim for a workload. | A single Deployment needs durable storage. |
job.yaml | One-shot task. | Migration, seed, setup, cleanup. |
cronjob.yaml | Scheduled task. | Periodic cleanup, sync, report, backup. |
tests/*.yaml | Helm test Pods or Jobs. | Release should include smoke checks. |
NOTES.txt | Post-install message. | Operators need commands, URLs, or follow-up steps. |
_helpers.tpl
Helpers are not rendered directly. They keep names, labels, and selectors stable across every manifest in the chart.
{{- define "app.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "app.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "app.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "app.selectorLabels" -}}
app.kubernetes.io/name: {{ include "app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}deployment.yaml
Use Deployments for stateless apps. This is where most production values appear: image, resources, probes, security contexts, lifecycle, scheduling, and termination behavior.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "app.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "app.selectorLabels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "app.fullname" . }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
startupProbe:
{{- toYaml .Values.startupProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}service.yaml
A Service gives Pods a stable DNS name and decouples callers from Pod IPs. Use named ports so Ingress and probes can refer to http instead of hard-coded numbers.
apiVersion: v1
kind: Service
metadata:
name: {{ include "app.fullname" . }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "app.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: httpconfigmap.yaml And secret.yaml
Put non-sensitive settings in ConfigMaps. Prefer existing or external secret systems for passwords and tokens; if the chart creates a Secret, keep it behind an explicit values switch.
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "app.fullname" . }}
data:
LOG_LEVEL: {{ .Values.config.logLevel | quote }}
FEATURE_FLAGS: {{ .Values.config.featureFlags | quote }}{{- if .Values.secret.create }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "app.fullname" . }}
type: Opaque
stringData:
DATABASE_URL: {{ .Values.secret.databaseUrl | quote }}
{{- end }}serviceaccount.yaml And RBAC
A named ServiceAccount makes identity explicit. Add Role and RoleBinding only when the Pod actually talks to the Kubernetes API.
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "app.fullname" . }}
annotations:
{{- toYaml .Values.serviceAccount.annotations | nindent 4 }}apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ include "app.fullname" . }}
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]ingress.yaml
Ingress exposes HTTP/S routes. Keep it conditional because internal workers and private services often do not need external routing.
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "app.fullname" . }}
annotations:
{{- toYaml .Values.ingress.annotations | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "app.fullname" . }}
port:
name: http
{{- end }}hpa.yaml And pdb.yaml
HPA adjusts replica count from metrics. PDB protects availability during voluntary disruptions such as node drains, but it only makes sense when enough replicas exist.
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "app.fullname" . }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "app.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}{{- if .Values.podDisruptionBudget.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "app.fullname" . }}
spec:
minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
selector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
{{- end }}mysql-statefulset.yaml
Use StatefulSets for databases and clustered systems that need stable Pod names, ordered rollouts, and per-Pod storage. A headless Service gives stable DNS such as mysql-0.mysql.
{{- if .Values.mysql.enabled }}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql
spec:
serviceName: mysql
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: mysql
template:
metadata:
labels:
app.kubernetes.io/name: mysql
spec:
containers:
- name: mysql
image: "{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}"
ports:
- name: mysql
containerPort: 3306
envFrom:
- secretRef:
name: mysql
volumeMounts:
- name: data
mountPath: /var/lib/mysql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: {{ toYaml .Values.mysql.persistence.accessModes | nindent 10 }}
resources:
requests:
storage: {{ .Values.mysql.persistence.size }}
{{- end }}networkpolicy.yaml
NetworkPolicy restricts Pod traffic when the CNI supports enforcement. Start with ingress allow rules for known callers, then add egress rules only when destinations are understood.
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "app.fullname" . }}
spec:
podSelector:
matchLabels:
{{- include "app.selectorLabels" . | nindent 6 }}
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: http
{{- end }}job.yaml And cronjob.yaml
Jobs handle one-time work such as migrations or seed data. CronJobs handle repeated work such as cleanup, reports, and syncs. Hook annotations can make a Job run during Helm lifecycle events.
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "app.fullname" . }}-migrate
annotations:
helm.sh/hook: pre-install,pre-upgrade
helm.sh/hook-delete-policy: before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./migrate"]apiVersion: batch/v1
kind: CronJob
metadata:
name: {{ include "app.fullname" . }}-cleanup
spec:
schedule: {{ .Values.cleanup.schedule | quote }}
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: cleanup
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["./cleanup"]tests And NOTES.txt
Helm tests give operators a release-level smoke check. NOTES.txt prints useful follow-up commands after install or upgrade.
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "app.fullname" . }}-smoke"
annotations:
helm.sh/hook: test
spec:
restartPolicy: Never
containers:
- name: curl
image: curlimages/curl:8.7.1
command: ["curl", "-f", "http://{{ include "app.fullname" . }}:{{ .Values.service.port }}/healthz"]Production values.yaml Shape
Expose knobs that operators can safely change per environment. Keep raw passwords out of Git, use immutable image tags or digests, and make availability settings match actual replica counts.
replicaCount: 3
image:
repository: registry.example.com/platform/web-api
tag: "sha-abc123"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
memory: 512Mi
podSecurityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
startupProbe:
httpGet:
path: /healthz
port: http
failureThreshold: 30
periodSeconds: 5
readinessProbe:
httpGet:
path: /readyz
port: http
periodSeconds: 10
livenessProbe:
httpGet:
path: /healthz
port: http
periodSeconds: 20
terminationGracePeriodSeconds: 30
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 10"]
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
podDisruptionBudget:
enabled: true
minAvailable: 2
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/name: web-api
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: web-api
nodeSelector: {}
tolerations: []
serviceAccount:
annotations: {}
ingress:
enabled: true
className: nginx
host: api.example.com
annotations: {}
config:
logLevel: info
featureFlags: "checkout-v2"
secret:
create: false
existingSecret: web-api-secrets
mysql:
enabled: false
image:
repository: mysql
tag: "8.0.36"
persistence:
accessModes: ["ReadWriteOnce"]
size: 20GiGotchas
- Do not template immutable selectors casually: changing Deployment or Service selectors can force replacement or break traffic.
- CPU limits are optional for many services: memory limits are important; CPU limits can create throttling if set without measurement.
- PDB and HPA must agree: a strict PDB with too few replicas blocks node drains.
- StatefulSets are not just Deployments with disks: storage, backup, restore, upgrades, and DNS identity need explicit operational design.
- Raw secrets do not belong in committed values: prefer External Secrets, Sealed Secrets, SOPS, or pre-created Secrets.