f in x
Helm for Kubernetes Practical Guide to Custom Charts and Professional Deployments
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Helm for Kubernetes Practical Guide to Custom Charts and Professional Deployments

[2026-06-16] Author: Ing. Calogero Bono

Your Kubernetes cluster is in production. Every deployment is a copy-paste of YAML files? Manifest files scattered across folders, hardcoded values, zero traceability. We see this often in projects we review: functioning clusters managed like prototypes. A syntax error in a Deployment and your app stays in CrashLoopBackOff for hours. The time spent fixing it is time you could have spent on your product.

We, at Meteora Web, have been working with Kubernetes for years — long before containers became the norm. We've seen companies save hundreds of hours simply by structuring deployments with Helm. And we've also seen the economic side: a failed manual deploy on a production cluster costs more than a day of development time. With Helm, deployment becomes reproducible, versioned, testable. And you pay for it once.

This guide is the second spoke of our pillar on Kubernetes and container orchestration. We'll take you inside Helm: from chart structure to best practices for real environments. No abstract theory — everything you need to stop writing YAML by hand and start using a proper package manager.

Why Helm Is Not Just a Nice-to-Have

Helm is the package manager for Kubernetes. But don't just think of it as 'apt for clusters'. It's much more: a templating engine, a release management system, a repository of ready-to-use packages. Without it, every deployment is a manual assembly: copy a YAML, paste, change namespace, modify name, cross your fingers. With Helm you define the structure once and parameterize it. The result? The same chart deployed in development, staging, and production with different values, zero code duplication.

The analogy we use with our clients is the cash register in a store: if you have to calculate every receipt by hand, you waste time and make mistakes. If you have an ERP that does it for you with fixed rules, you gain precision and speed. Helm is your ERP for Kubernetes.

Sponsored Protocol

The Cost of Not Using Helm

A client brought us a cluster with 15 microservices, each with its own folder of YAML files. Every deployment required between 20 and 40 minutes of human attention. A typo in a volume name — and the pod wouldn't start. Helm reduced deployment to a single helm upgrade --install command. The savings? About 15 hours per month. That's not just technology — it's economics.

Anatomy of an Helm Chart

A chart is a structured directory. If you don't respect it, Helm won't parse it. Here are the mandatory and typical files:

my-chart/
├── Chart.yaml          # chart metadata (name, version, dependencies)
├── values.yaml         # default values for templates
├── templates/          # Go template files that generate YAML
│   ├── deployment.yaml
│   ├── service.yaml
│   └── _helpers.tpl    # reusable functions (underscore = no resource generated)
├── charts/             # dependencies (nested charts)
└── .helmignore         # files to exclude

Chart.yaml is the identity card. Minimal example:

apiVersion: v2
name: my-webapp
description: A simple web server
version: 0.1.0
appVersion: "1.16.0"

values.yaml is where you put values that change per environment: replicaCount, image, port, resources. It's the heart of parameterization.

templates/ contains files processed by the Go template engine. Any file starting with _ does not produce a Kubernetes object but can be included by other templates (e.g., helpers for common labels).

Common mistake: forgetting to version control charts in git. The chart must be under version control, exactly like code. Otherwise you lose reproducibility.

Create Your First Chart from Scratch

All you need is a terminal. Use helm create to generate a sample structure, but we prefer to start from zero to understand everything.

Sponsored Protocol

Step 1: the directory

mkdir nginx-private
cd nginx-private
mkdir templates

Step 2: Chart.yaml

apiVersion: v2
name: nginx-private
description: My custom nginx with parameterized values
version: 0.1.0
appVersion: "1.25.0"

Step 3: values.yaml

replicaCount: 2
image:
  repository: nginx
  tag: "1.25.0"
  pullPolicy: IfNotPresent
service:
  type: ClusterIP
  port: 80
resources: {}
nodeSelector: {}

Step 4: deployment.yaml in templates/

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "nginx-private.fullname" . }}
  labels:
    {{- include "nginx-private.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "nginx-private.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "nginx-private.selectorLabels" . | nindent 8 }}
    spec:
      containers:
        - name: nginx
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - containerPort: {{ .Values.service.port }}

Step 5: service.yaml

apiVersion: v1
kind: Service
metadata:
  name: {{ include "nginx-private.fullname" . }}
  labels:
    {{- include "nginx-private.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: {{ .Values.service.port }}
      protocol: TCP
      name: http
  selector:
    {{- include "nginx-private.selectorLabels" . | nindent 4 }}

Step 6: _helpers.tpl (standard names and labels)

{{- define "nginx-private.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "nginx-private.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{- define "nginx-private.labels" -}}
helm.sh/chart: {{ include "nginx-private.name" . }}-{{ .Chart.Version | replace "+" "_" }}
{{ include "nginx-private.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "nginx-private.selectorLabels" -}}
app.kubernetes.io/name: {{ include "nginx-private.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Now you can install the chart with:

Sponsored Protocol

helm install my-nginx ./nginx-private

Go Template Deep Dive: What You Really Need

Helm templates use Go language with Sprig functions. You don't need to be a Go expert, but you need to know the key functions.

Accessing values

.Values.replicaCount — the dot refers to the global object. .Chart, .Release, .Files are other built-in objects.

Conditionals

Useful for exposing services only when needed:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
...
{{- end }}

Range (loops)

To iterate over a list, for example multiple environment variables:

env:
{{- range $key, $val := .Values.envVars }}
  - name: {{ $key }}
    value: {{ $val | quote }}
{{- end }}

String manipulation functions

upper, lower, trimSuffix, quote, default, required. required is great for validating that a mandatory value is present:

domain: {{ required "A value for domain is required" .Values.domain }}

Whitespace and indentation

Use {{- to trim preceding whitespace and -}} to trim trailing. nindent adds a newline and indentation. Golden rule: for items in a list, use nindent to keep valid YAML.

Sponsored Protocol

Managing Dependencies: Charts Containing Charts

Your webapp needs a database? Instead of writing a Deployment for MySQL by hand, you can declare a dependency on an official chart (e.g., bitnami/mysql).

Chart.yaml with dependencies

apiVersion: v2
name: webapp-with-db
dependencies:
  - name: mysql
    version: "9.10.2"
    repository: "https://charts.bitnami.com/bitnami"
    condition: mysql.enabled
    tags:
      - database

Then run:

helm dependency update

It downloads the chart into the charts/ directory. Now you can pass configuration values for MySQL in your values.yaml under the mysql key.

Caution: dependencies are not updated automatically. When the parent chart version changes, you must explicitly run helm dep update again.

Deploy and Rollback: The Command That Saves the Day

The Helm lifecycle is managed with helm install, helm upgrade, helm rollback. Each deployment is a release with a revision number.

Installation

helm install my-webapp ./my-chart -f values-production.yaml --namespace production --create-namespace

Upgrade

helm upgrade my-webapp ./my-chart -f values-production.yaml --atomic --timeout 10m

The --atomic flag rolls back the previous release if the upgrade fails. Don't fly without a net.

Rollback

helm rollback my-webapp 2  # go back to revision 2
helm history my-webapp    # list revisions

Best Practices for Custom Charts

  • Use standard naming: app.kubernetes.io/name, app.kubernetes.io/instance, app.kubernetes.io/version. Monitoring and networking tools expect these labels.
  • Validate values with JSON Schema: create a values.schema.json file at the chart root. Helm uses it to validate values before rendering. A simple schema prevents errors like replicaCount: "three".
  • Don't hardcode names: always use helpers to generate fullname. Avoids collisions in shared namespaces.
  • Separate values per environment: files like values-dev.yaml, values-prod.yaml are separate from the chart. The chart is the single source of truth for structure; values are configuration.
  • Update appVersion: when the container tag changes, update appVersion in Chart.yaml and increment version. That way the operations team instantly knows if the chart is aligned.

Integration with CI/CD

Helm shines in automated pipelines. Once the container image is built, the pipeline can run helm upgrade --install with the right values. We dive deeper into this topic in our dedicated CI/CD article.

Sponsored Protocol

We also recommend reading our Kubernetes pillar guide to frame Helm in the full orchestration context.

In Summary — What to Do Now

  1. Convert one of your manual deployments into an Helm chart. Take the simplest Deployment, Service, and ConfigMap you have and create a chart following the steps in this guide.
  2. Parameterize values: move into values.yaml everything that changes per environment (replicas, image, environment variables, resources).
  3. Add helpers for names: don't use fixed names, use include "your-chart.fullname" .
  4. Add a JSON Schema: a simple values.schema.json file at the chart root reduces configuration errors.
  5. Integrate Helm into your CI/CD pipeline: if you already use GitLab CI, GitHub Actions, or Jenkins, add a step that runs helm upgrade --install --atomic.

And if you have doubts about your cluster, the hidden costs of manual deployment, or the security of your manifests, let's talk. We've been working with Kubernetes long before it was fashionable.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()