
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Managing raw Kubernetes manifests quickly becomes unmanageable as environments multiply and configuration drift sets in. When you write your first Helm chart, you move from static YAML files to a parameterized, version-controlled package that adapts to dev, staging, and production without duplication. This guide walks through creating a functional chart from scratch, focusing on the template mechanics and values structure that actually matter in production.
helm create mychart to scaffold the standard directory structure, then customize values.yaml for defaults and edit templates in the templates/ folder using Go templating syntax. Validate with helm lint and helm template before installing to ensure your Kubernetes resources render correctly across environments.How do you scaffold a new Helm chart correctly?
The helm create command generates a production-grade skeleton that follows established conventions. While many tutorials treat this as a mere starting point, understanding what it creates prevents structural mistakes later. If you are also exploring Helm charts for Kubernetes packaging, recognizing this standard layout helps you read third-party charts confidently.
helm create my-app
cd my-app
tree . This produces the canonical structure:
- Chart.yaml: Metadata including name, version (SemVer), appVersion, and dependencies.
- values.yaml: Default configuration values; this is your primary interface.
- templates/: Go template files that generate Kubernetes manifests.
- templates/_helpers.tpl: Reusable template definitions and naming conventions.
- charts/: Directory for dependent sub-charts.
- .helmignore: Patterns for files to exclude from packaging.
A common mistake is modifying the generated deployment template without understanding its helper functions. The default _helpers.tpl defines my-app.fullname, my-app.labels, and my-app.selectorLabels. These ensure consistent labeling across all resources, which is critical for service discovery and upgrades. Never hardcode names or labels directly in resource templates; always reference these helpers to maintain upgrade safety.
How do you manage configuration with values.yaml?
The values.yaml file is the contract between chart authors and users. When you write your first Helm chart, structure this file for clarity and override-friendliness rather than exhaustive documentation. Group related settings logically and use nested maps for complex configurations.
# values.yaml
replicaCount: 2
image:
repository: nginx
tag: "1.25-alpine"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80 In practice, avoid flat key structures like imageRepository and imageTag. Nested objects allow selective overrides: a user can change only image.tag in a staging values file without re-specifying the repository. Always provide sensible defaults that work out-of-the-box for local development; environment-specific concerns belong in separate override files passed via -f staging-values.yaml.
For teams managing multiple environments, consider the precedence order: built-in defaults < chart values.yaml < parent chart values < user-supplied values files < --set flags. Document non-obvious keys in comments directly above them, but keep comments concise. If a value requires extensive explanation, it likely needs restructuring or belongs in external documentation linked from Chart.yaml.
How do you write safe and reusable Helm templates?
Helm uses Go’s text/template engine with Sprig functions. The most frequent errors when engineers write their first Helm chart stem from misunderstanding scope, missing required fields, or producing invalid YAML. Always wrap template expressions that output strings in quotes unless the field explicitly requires an unquoted integer or boolean.
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: {{ .Values.service.targetPort }}
protocol: TCP
resources:
{{- toYaml .Values.resources | nindent 12 }} Key patterns to internalize:
- Use
nindentoverindent:nindentadds a newline before indenting, preventing YAML parsing errors when blocks follow other keys. - Quote string outputs: Image tags, annotations, and label values should be quoted to handle special characters and numeric-looking strings safely.
- Validate required values: Use
required "message" .Values.someKeyto fail fast with a clear error instead of rendering empty strings. - Prefer
toYamlfor nested objects: Resources, node selectors, tolerations, and affinity rules should be injected as complete YAML blocks rather than reconstructed key-by-key.
Always test templates locally before pushing. The helm template ./my-app -f staging-values.yaml command renders manifests without contacting a cluster, letting you inspect the exact output. Pair this with helm lint ./my-app to catch structural issues, undefined references, and YAML syntax errors. In CI pipelines, add both commands as mandatory gates; catching template failures pre-deploy saves hours of debugging broken releases.
How do you validate and test your Helm chart before release?
Validation separates working charts from fragile ones. Beyond basic linting, adopt a layered testing approach that mirrors how you would test application code. For teams integrating Helm into CI/CD pipeline automation workflows, automated chart validation prevents configuration regressions from reaching production clusters.
| Validation Method | Purpose | When to Run |
|---|---|---|
helm lint | Catches syntax errors, missing required values, and structural issues | Every commit, pre-commit hook |
helm template | Renders full manifests for visual inspection and diffing | PR review, CI artifact generation |
helm install --dry-run | Simulates installation against live cluster API without applying | Pre-deployment verification |
ct lint/install | Chart Testing tool validates version bumps and fresh installs | PR checks for chart changes |
kubeval / kubeconform | Validates rendered YAML against Kubernetes OpenAPI schemas | CI pipeline post-render step |
A critical practice often overlooked is testing upgrade paths. Run helm upgrade --install my-app ./my-app --dry-run against an existing release to verify that label selectors remain stable and persistent volume claims aren’t accidentally recreated. Selector immutability is the most common cause of failed upgrades; if you change matchLabels in a Deployment, Kubernetes rejects the update. Always include selector labels in your _helpers.tpl and never expose them as configurable values.
For charts intended for team-wide use, add unit tests using the helm-unittest plugin. Tests assert that specific values produce expected manifest fields, catching logic errors in conditionals and range loops. This is especially valuable when supporting multiple Kubernetes versions or cloud providers where API fields differ.
How do you package and distribute your Helm chart?
Once validated, package your chart for distribution. The helm package ./my-app command creates a versioned .tgz archive named according to Chart.yaml. Versioning follows Semantic Versioning strictly: increment the major version for breaking changes (removed values, renamed templates), minor for backward-compatible additions, and patch for bug fixes.
# Update version in Chart.yaml before packaging
version: 0.2.0
appVersion: "1.25.0"
# Package and push to OCI registry (recommended for 2026)
helm package ./my-app
helm push my-app-0.2.0.tgz oci://registry.example.com/charts OCI registries have become the standard for chart distribution, replacing legacy HTTP repositories. They integrate with existing container registry infrastructure, support authentication via standard Docker credentials, and enable signed artifacts using cosign or Notation. If you are exploring AI-assisted IaC generation, OCI-based charts simplify pipeline integration since the same registry handles both images and charts.
Always sign your chart packages before publishing. Supply chain security is non-negotiable in 2026; unsigned charts should be treated as untrusted. Use helm signtool or cosign to attach signatures, and configure your CI to verify signatures before deployment. Document the signing key fingerprint in your chart’s README so consumers can validate authenticity independently.
Write Your First Helm Chart With Production Discipline
Writing your first Helm chart is straightforward; writing one that survives production demands discipline around templating safety, values design, and validation rigor. Start with helm create, respect the helper conventions, structure values for override flexibility, and never skip linting and dry-run testing. Treat your chart as software: version it semantically, test it automatically, sign it cryptographically, and document its contract clearly. If you need help designing charts that meet compliance requirements or integrate with existing GitOps workflows, reach out to discuss your infrastructure packaging needs.