How to Install Helm on Ubuntu 24.04 VPS: The Kubernetes Package Manager
Helm is the de-facto package manager for Kubernetes. Where kubectl apply forces you to hand-assemble dozens of YAML manifests, Helm bundles them into a single versioned artifact called a chart -- one command to install, one command to upgrade, one command to roll back. If you run a self-hosted Kubernetes cluster on a Ubuntu 24.04 VPS (typically with k3s or kubeadm), Helm is the tool you reach for every single day.
This guide walks you through installing Helm on Ubuntu 24.04, connecting it to your cluster, adding the most useful public chart repositories (Bitnami and ingress-nginx), installing your first production workload, writing your own values.yaml overrides, and finally authoring a chart of your own. By the end, you will understand how to upgrade, roll back, template-debug, and compose multi-chart releases with Helmfile or umbrella charts.
Need a cluster to install Helm against? The CloudCore Starter plan is the sweet spot for a single-node k3s + Helm lab. Pair it with our k3s install guide and you will have a working cluster in under ten minutes.
Table of Contents
What Is Helm?
Helm is an open-source package manager for Kubernetes maintained by the CNCF. It solves a problem every Kubernetes user hits within their first week: a real-world application is not one YAML file, it is 15 -- Deployments, Services, ConfigMaps, Secrets, Ingresses, ServiceAccounts, RBAC rules, HorizontalPodAutoscalers, NetworkPolicies, PersistentVolumeClaims, and more. Shipping these as loose files is unmanageable at any non-trivial scale.
Helm introduces three core concepts. A chart is a directory (or tarball) containing templated Kubernetes manifests plus metadata describing the application. A release is a specific installation of a chart into a cluster, tracked by Helm with a name, revision number, and stored history. A repository is an HTTP-accessible index of charts, typically served from GitHub Pages, OCI registries, or a dedicated chart museum.
With Helm you run helm install my-postgres bitnami/postgresql and Helm applies the full set of manifests atomically. Change one value, run helm upgrade, and Helm diffs the new desired state against the previous release, applies only the delta, and bumps the revision. Something broke? helm rollback restores the prior manifest set in seconds. This is the same ergonomic model apt brings to Debian/Ubuntu, but for distributed systems.
The public chart ecosystem is enormous. Bitnami (now part of VMware/Broadcom) maintains hardened charts for PostgreSQL, MySQL, MongoDB, Redis, RabbitMQ, Kafka, Elasticsearch, WordPress, Nextcloud, Keycloak, and 100+ more. ingress-nginx provides the canonical ingress controller chart. cert-manager, Prometheus, Grafana, Loki, ArgoCD, Jaeger, Vault, MinIO, Traefik, and nearly every major cloud-native project ships an official chart. You very rarely need to write manifests from scratch.
Why Self-Host Kubernetes + Helm Instead of a Managed Service?
Managed Kubernetes (EKS, GKE, AKS, DigitalOcean DOKS, Linode LKE) is convenient, but it comes at a cost that small and mid-size teams frequently underestimate. Here is the honest trade-off:
- Flat, predictable pricing. A managed control plane alone is typically USD 70-150/month before you pay for a single worker node, load balancer, or gigabyte of egress. A self-hosted k3s cluster on a CloudCore Starter VPS runs the full control plane + worker on one box for a fraction of that, with unmetered bandwidth included.
- No per-cluster fees, no per-pod metering. Scale to 200 pods or 2 pods, you pay the same EUR amount every month.
- No vendor lock-in. Everything -- the control plane, the container runtime, the CNI, the CSI, Helm itself -- is open source. You can migrate between providers by re-running your Helmfile against the new cluster.
- Root access to every layer. Need to tweak kubelet flags, swap out containerd for CRI-O, add a custom CNI plugin, or debug kernel-level networking issues? On managed Kubernetes you file a support ticket. On your own VPS you
sshin and fix it. - Data residency and compliance. You choose the datacenter. You control the disks. You decide what logs leave the machine. For GDPR, HIPAA-adjacent, or financial workloads, this is not optional.
- Learning compounds. The skills you build operating a self-hosted cluster transfer directly to any Kubernetes job, whereas cloud-provider-specific knowledge (EKS add-ons, GKE Autopilot quirks) is partially disposable.
Cost Comparison: Self-Hosted vs. Managed Kubernetes
| Line item | Managed Kubernetes (typical) | Self-Hosted k3s + Helm (CloudCore Starter) |
|---|---|---|
| Control plane fee | ~USD 75/month | Included (runs on the same VPS) |
| Worker node (4 vCPU / 8 GB) | ~USD 40/month | Included |
| Load balancer | ~USD 15/month each | Included (ServiceLB or MetalLB) |
| Bandwidth (1 TB/mo) | USD 20-90 | Unmetered |
| Helm-installed charts | Unlimited | Unlimited |
| Typical monthly total | USD 150+ | EUR 7.99 |
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access.
- SSH access to the server.
- A working Kubernetes cluster reachable from the VPS where you install Helm. If you do not have one yet, follow How to Install k3s on Ubuntu 24.04 first -- you can install Helm on the same node as k3s.
kubectlconfigured with a valid~/.kube/configpointing at your cluster. Verify withkubectl get nodes.- At least 2 GB of free RAM on the cluster nodes to install non-trivial charts like ingress-nginx or PostgreSQL.
Recommended Plan: CloudCore Starter>
For a single-node k3s + Helm learning environment or a small production cluster, the CloudCore Starter plan is ideal:>
- 4 vCPU cores
- 8 GB RAM
- 75 GB NVMe SSD
- Unmetered bandwidth
- From EUR 7.99/month>
This is enough headroom to run k3s, Helm, ingress-nginx, cert-manager, and a handful of application charts simultaneously without swapping. For multi-node HA clusters, upgrade to CloudCore Professional and add worker nodes as you grow.
Connect to your server:
ssh root@your-server-ipStep 1: Update System Packages
Refresh the package index and apply any pending security updates so the APT repository metadata you add later resolves cleanly.
sudo apt update && sudo apt upgrade -yExpected output (abbreviated):
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.Install the packages Helm's APT setup depends on:
sudo apt install -y curl gnupg apt-transport-https ca-certificatesIf your kernel was upgraded, reboot before continuing:
sudo rebootStep 2: Install Helm via the APT Repository
The Helm project publishes a signed Debian/Ubuntu APT repository. This is the recommended approach for production because it integrates Helm into apt upgrade and ensures you receive signed upgrades.
Import the Helm signing key:
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/nullAdd the repository to APT:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" \
| sudo tee /etc/apt/sources.list.d/helm-stable-debian.listUpdate the package index and install Helm:
sudo apt update
sudo apt install -y helmExpected output:
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
helm
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
Need to get 15.8 MB of archives.
After this operation, 56.2 MB of additional disk space will be used.
...
Setting up helm (3.15.4-1) ...From now on, sudo apt upgrade keeps Helm on the latest stable release alongside the rest of your system.
Step 3: Alternative Install via the Official Script
If you prefer a single-command install, or you are provisioning via cloud-init where adding APT keys is awkward, the official get-helm-3 script downloads the correct binary for your architecture and drops it at /usr/local/bin/helm.
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.shExpected output:
Downloading https://get.helm.sh/helm-v3.15.4-linux-amd64.tar.gz
Verifying checksum... Done.
Preparing to install helm into /usr/local/bin
helm installed into /usr/local/bin/helmTo pin a specific version, pass --version:
./get_helm.sh --version v3.14.4The script approach is faster and scriptable, but it does not add a package source, so apt upgrade will not update Helm. You are responsible for re-running the script to upgrade. Pick APT for long-lived servers, script for ephemeral CI runners or cloud-init.
Step 4: Verify Helm and Cluster Connectivity
Confirm Helm is on your $PATH and can talk to your cluster.
Check the installed version:
helm versionExpected output:
version.BuildInfo{Version:"v3.15.4", GitCommit:"fa9efb07d9d8debbb4306d72af76a383895aa8c4", GitTreeState:"clean", GoVersion:"go1.22.6"}Helm reads your ~/.kube/config the same way kubectl does. Verify the cluster is reachable:
kubectl get nodesExpected output (for a single-node k3s install):
NAME STATUS ROLES AGE VERSION
cc-01 Ready control-plane,master 3d v1.30.3+k3s1Check that Helm sees no releases yet:
helm list --all-namespacesExpected output:
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSIONAn empty table is the correct result on a fresh cluster. You are ready to add repositories.
Step 5: Add Chart Repositories (Bitnami + ingress-nginx)
Helm 3 requires you to explicitly add any repository you want to install from. Two repos cover 80% of day-one use cases: Bitnami for databases, message queues, and common applications, and ingress-nginx for the ingress controller that exposes your services to the internet.
Add the Bitnami repo:
helm repo add bitnami https://charts.bitnami.com/bitnamiExpected output:
"bitnami" has been added to your repositoriesAdd the ingress-nginx repo:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginxExpected output:
"ingress-nginx" has been added to your repositoriesRefresh the local index cache so Helm knows what charts and versions each repo offers:
helm repo updateExpected output:
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "ingress-nginx" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. Happy Helming!List repositories:
helm repo listExpected output:
NAME URL
bitnami https://charts.bitnami.com/bitnami
ingress-nginx https://kubernetes.github.io/ingress-nginxSearch for a chart by keyword:
helm search repo postgresqlExpected output (abbreviated):
NAME CHART VERSION APP VERSION DESCRIPTION
bitnami/postgresql 15.5.20 16.3.0 PostgreSQL (Postgres) is an open source object-...
bitnami/postgresql-ha 14.3.1 16.3.0 This PostgreSQL cluster solution includes the P...Other popular repos worth adding as you expand your stack:
helm repo add jetstack https://charts.jetstack.io # cert-manager
helm repo add argo https://argoproj.github.io/argo-helm # ArgoCD
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo updateStep 6: Install Your First Chart
Install the ingress-nginx controller. Every cluster needs an ingress controller to route HTTP(S) traffic from the outside world to internal services.
Create a dedicated namespace and install:
kubectl create namespace ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx \ --set controller.service.type=LoadBalancer \ --set controller.publishService.enabled=true
Expected output:
NAME: ingress-nginx
LAST DEPLOYED: Thu Apr 16 10:00:00 2026
NAMESPACE: ingress-nginx
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
The ingress-nginx controller has been installed.
It may take a few minutes for the LoadBalancer IP to be available.Break down the command:
ingress-nginx(first argument) -- the release name. You choose this. It must be unique per namespace.ingress-nginx/ingress-nginx--repo/chart. The first is the repo alias you added in Step 5, the second is the chart name inside that repo.--namespace-- where to install. Helm will not create the namespace unless you pass--create-namespace.--set key=value-- inline override of a default value. Use sparingly; prefervalues.yamlfor anything beyond a quick experiment.
kubectl get pods -n ingress-nginx -wExpected output:
NAME READY STATUS RESTARTS AGE
ingress-nginx-controller-7f9b8c5d4f-k2xjp 1/1 Running 0 45sConfirm the release is tracked by Helm:
helm list -n ingress-nginxExpected output:
NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION
ingress-nginx ingress-nginx 1 2026-04-16 10:00:00 deployed ingress-nginx-4.11.2 1.11.2You just deployed roughly a dozen Kubernetes objects (Deployment, Service, ConfigMap, ServiceAccount, ClusterRole, ClusterRoleBinding, Role, RoleBinding, ValidatingWebhookConfiguration, Job, IngressClass, and more) with a single command. That is the Helm value proposition in action.
Step 7: Customize with values.yaml
--set flags are fine for tweaking one or two values, but production configuration belongs in a values.yaml file you commit to Git. Every chart exposes its tunable parameters through values.
See the full set of defaults a chart supports:
helm show values bitnami/postgresql > postgresql-defaults.yaml
wc -l postgresql-defaults.yamlExpected output:
1247 postgresql-defaults.yamlThat is over 1,200 configuration knobs. You do not override all of them -- you create a small file that overrides only what you need. Create postgres-values.yaml:
# postgres-values.yaml auth: username: dmapp database: dmapp existingSecret: postgres-credentials # created separately via kubectlprimary: persistence: enabled: true size: 20Gi storageClass: local-path # k3s default resources: requests: cpu: 250m memory: 512Mi limits: cpu: 1000m memory: 2Gi
metrics: enabled: true serviceMonitor: enabled: false # enable once Prometheus Operator is installed
Create the credentials secret Helm will reference:
kubectl create namespace databases
kubectl -n databases create secret generic postgres-credentials \
--from-literal=postgres-password="$(openssl rand -base64 24)" \
--from-literal=password="$(openssl rand -base64 24)"Install the chart with your custom values:
helm install postgres bitnami/postgresql \
--namespace databases \
--values postgres-values.yamlExpected output:
NAME: postgres
LAST DEPLOYED: Thu Apr 16 10:05:00 2026
NAMESPACE: databases
STATUS: deployed
REVISION: 1Verify:
kubectl -n databases get pods,pvc,svcExpected output:
NAME READY STATUS RESTARTS AGE pod/postgres-postgresql-0 1/1 Running 0 90sNAME STATUS VOLUME CAPACITY persistentvolumeclaim/data-postgres-postgresql-0 Bound pvc-... 20Gi
NAME TYPE CLUSTER-IP PORT(S) service/postgres-postgresql ClusterIP 10.43.142.17 5432/TCP service/postgres-postgresql-hl ClusterIP None 5432/TCP
Values Precedence
Helm merges values from multiple sources in this order (lowest to highest precedence):
values.yamlvalues.yaml (for umbrella charts)--values file.yaml (can be passed multiple times, later files win)--set key=value flags on the command lineThis means you can layer a base values.yaml, a per-environment values-production.yaml, and a last-minute --set image.tag=hotfix-42 all in one helm upgrade call.
Step 8: Upgrade and Roll Back Releases
The superpower Helm gives you over raw kubectl apply is versioned, reversible releases. Every helm upgrade produces a new revision; every revision is stored in the cluster; any revision can be restored in seconds.
Change resources.limits.memory in postgres-values.yaml from 2Gi to 4Gi, then:
helm upgrade postgres bitnami/postgresql \
--namespace databases \
--values postgres-values.yamlExpected output:
Release "postgres" has been upgraded. Happy Helming!
NAME: postgres
LAST DEPLOYED: Thu Apr 16 10:15:00 2026
NAMESPACE: databases
STATUS: deployed
REVISION: 2View the revision history:
helm history postgres -n databasesExpected output:
REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
1 2026-04-16 10:05:00 UTC superseded postgresql-15.5.20 16.3.0 Install complete
2 2026-04-16 10:15:00 UTC deployed postgresql-15.5.20 16.3.0 Upgrade completeSomething went wrong with revision 2? Roll back to revision 1:
helm rollback postgres 1 -n databasesExpected output:
Rollback was a success! Happy Helming!Helm creates revision 3 that matches the manifest state of revision 1. The history is append-only, so you always know what ran when.
Useful Upgrade Flags
--install-- install if the release does not exist, upgrade otherwise. Perfect for CI pipelines:helm upgrade --install.--atomic-- roll back automatically if the upgrade fails or times out. Essential for production.--timeout 5m-- how long to wait for resources to become ready. Default is 5 minutes.--wait-- block until all Deployments, StatefulSets, and Services reach ready state before marking the release as deployed.--dry-run-- render the templates and show what would be sent to the API server, without actually applying.--diff-- with thehelm-diffplugin (helm plugin install https://github.com/databus23/helm-diff), shows a unified diff between the current release and the proposed upgrade.
helm upgrade --install postgres bitnami/postgresql \
--namespace databases \
--values postgres-values.yaml \
--atomic \
--wait \
--timeout 10mStep 9: Inspect Rendered Templates
When a chart misbehaves, the first diagnostic step is to see exactly what YAML Helm is generating. Use helm template to render templates locally without touching the cluster.
Render an entire chart with your values:
helm template postgres bitnami/postgresql \
--values postgres-values.yaml \
--namespace databasesThe output is the complete set of Kubernetes manifests Helm would apply. Pipe it into kubectl diff or into kubeval/kubeconform for schema validation.
Render only one template file (useful when a chart has 30+ templates):
helm template postgres bitnami/postgresql \
--values postgres-values.yaml \
--show-only templates/primary/statefulset.yamlSee what an existing release looks like after rendering:
helm get manifest postgres -n databasesSee the values a release was installed with:
helm get values postgres -n databasesSee the full values tree (user overrides merged with defaults):
helm get values postgres -n databases --allLint a local chart directory for syntax and best-practice issues:
helm lint ./my-chartThese commands turn a Helm release from a black box into something fully inspectable.
Step 10: Create Your Own Chart
At some point you stop installing other people's charts and start packaging your own application. helm create scaffolds a working chart you can edit.
helm create dm-webExpected output:
Creating dm-webInspect the generated directory:
tree dm-webExpected output:
dm-web
├── Chart.yaml
├── charts
├── templates
│ ├── NOTES.txt
│ ├── _helpers.tpl
│ ├── deployment.yaml
│ ├── hpa.yaml
│ ├── ingress.yaml
│ ├── service.yaml
│ ├── serviceaccount.yaml
│ └── tests
│ └── test-connection.yaml
├── values.yaml
└── .helmignoreKey files:
Chart.yaml-- metadata: name, version, appVersion, dependencies, maintainers, home URL.values.yaml-- default configuration values users of your chart can override.templates/-- Go-templated Kubernetes manifests. Files starting with_are partials/helpers, not standalone manifests.templates/_helpers.tpl-- reusable template snippets for labels, selector labels, service account names.templates/NOTES.txt-- post-install instructions printed to the user afterhelm install.charts/-- subchart dependencies (populated byhelm dependency update).
Customize Chart.yaml
Edit dm-web/Chart.yaml to describe your application:
apiVersion: v2
name: dm-web
description: The DM marketing site and storefront
type: application
version: 0.1.0 # chart version (SemVer, bumps independently)
appVersion: "1.0.0" # app version you are packaging
maintainers:
- name: DM DevOps
email: [email protected]
home: https://vps-server.host
icon: https://vps-server.host/logo.pngUnderstand the Templating Language
Open templates/deployment.yaml. Helm uses Go's text/template with custom functions from the Sprig library plus Helm-specific additions.
Snippet:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "dm-web.fullname" . }}
labels:
{{- include "dm-web.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "dm-web.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "dm-web.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCPKey constructs:
{{ .Values.xxx }}-- reference a value fromvalues.yaml(or user overrides).{{ .Chart.Name }},{{ .Chart.AppVersion }}-- metadata fromChart.yaml.{{ .Release.Name }},{{ .Release.Namespace }}-- information about this specific installation.{{ include "dm-web.fullname" . }}-- call a named template defined in_helpers.tpl.{{- ... -}}-- the hyphens trim whitespace and the preceding/following newline.| nindent 4-- pipe the result through a function that indents every line by 4 spaces, with a leading newline.| default .Chart.AppVersion-- fall back to.Chart.AppVersionif the value is empty.
Install Your Chart
helm install dm-web ./dm-web \
--namespace apps --create-namespace \
--set image.repository=ghcr.io/vps-server-host/dm-web \
--set image.tag=v1.0.0Package and Publish
Package the chart into a versioned tarball:
helm package dm-webExpected output:
Successfully packaged chart and saved it to: /root/dm-web-0.1.0.tgzYou can host the tarball on any HTTP server, GitHub Pages, S3, or push it to an OCI registry like ghcr.io or Harbor:
helm push dm-web-0.1.0.tgz oci://ghcr.io/vps-server-host/chartsConsumers then install with:
helm install dm-web oci://ghcr.io/vps-server-host/charts/dm-web --version 0.1.0Step 11: Helmfile and Umbrella Charts
A production cluster rarely runs one chart. It runs 10-30: ingress, cert-manager, monitoring, logging, databases, message queues, applications. You need a way to declare the set of releases as code. Two patterns dominate.
Umbrella Charts
An umbrella chart is a Helm chart whose only purpose is to depend on other charts. Create platform/Chart.yaml:
apiVersion: v2
name: platform
version: 0.1.0
type: application
dependencies:
- name: ingress-nginx
version: "4.11.2"
repository: "https://kubernetes.github.io/ingress-nginx"
- name: cert-manager
version: "v1.15.3"
repository: "https://charts.jetstack.io"
condition: cert-manager.enabled
- name: postgresql
version: "15.5.20"
repository: "https://charts.bitnami.com/bitnami"
alias: primary-dbFetch the dependencies:
helm dependency update ./platformThis downloads each dependency tarball into platform/charts/. Install the whole stack as one release:
helm install platform ./platform -n platform --create-namespaceOverride values per subchart in platform/values.yaml:
ingress-nginx: controller: service: type: LoadBalancercert-manager: enabled: true installCRDs: true
primary-db: # matches the alias above auth: database: platform primary: persistence: size: 50Gi
Umbrella charts are Helm-native and require no extra tooling. The downside: they couple everything into one release, so a failure in any subchart can roll back the whole stack.
Helmfile
Helmfile solves the same problem by declaring releases in a separate YAML file outside Helm itself. Install:
curl -L https://github.com/helmfile/helmfile/releases/download/v0.167.1/helmfile_0.167.1_linux_amd64.tar.gz \
| sudo tar -xz -C /usr/local/bin helmfileCreate helmfile.yaml:
repositories: - name: ingress-nginx url: https://kubernetes.github.io/ingress-nginx - name: jetstack url: https://charts.jetstack.io - name: bitnami url: https://charts.bitnami.com/bitnamireleases: - name: ingress-nginx namespace: ingress-nginx createNamespace: true chart: ingress-nginx/ingress-nginx version: 4.11.2 values: - ./values/ingress-nginx.yaml
- name: cert-manager namespace: cert-manager createNamespace: true chart: jetstack/cert-manager version: v1.15.3 values: - installCRDs: true
- name: postgres namespace: databases createNamespace: true chart: bitnami/postgresql version: 15.5.20 values: - ./values/postgres.yaml needs: - cert-manager/cert-manager # deploy after cert-manager
Apply the full stack:
helmfile applyHelmfile installs each release as an independent Helm release, respects the needs ordering, and prints a diff before applying. This is the pattern most production teams adopt because it scales cleanly, integrates with GitOps workflows, and makes per-environment overrides trivial (helmfile -e production apply, helmfile -e staging apply).
For a full GitOps workflow that automatically reconciles your Helmfile (or plain Helm releases) from a Git repo, see our companion guide How to Install ArgoCD on Ubuntu 24.04. For automatic TLS issuance across every ingress you deploy, install cert-manager as the second chart in your stack.
Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
Error: Kubernetes cluster unreachable | ~/.kube/config missing, wrong context, or API server unreachable | Check kubectl cluster-info. Set KUBECONFIG=/etc/rancher/k3s/k3s.yaml for k3s. Verify the current context: kubectl config current-context. |
Error: INSTALLATION FAILED: cannot re-use a name that is still in use | A release with that name already exists in the namespace | List releases: helm list -A. Either pick a different name or run helm upgrade instead of helm install. |
Error: UPGRADE FAILED: another operation (install/upgrade/rollback) is in progress | A previous Helm operation crashed and left a pending lock | Check: helm list -A --pending. Mark as failed: kubectl -n <ns> rollout status on the resources, then helm rollback <release> <prev-revision> or delete the stuck secret: kubectl -n <ns> delete secret -l owner=helm,name=<release>,status=pending-upgrade. |
Error: failed to download "bitnami/postgresql" | Repo index is stale or the chart version no longer exists | helm repo update. Check available versions: helm search repo bitnami/postgresql --versions. |
| Template renders wrong indentation | Missing nindent vs indent, or misplaced {{- whitespace trim | Render with helm template and inspect. Remember: nindent N adds a newline then indents; indent N only indents. |
| Values not taking effect | Wrong key path or YAML type mismatch | helm get values <release> to see what Helm thinks the values are. Compare against helm show values <chart> for the canonical key paths. |
pull access denied on OCI chart | Not logged into the OCI registry | helm registry login ghcr.io -u <user> before helm install oci://.... |
| CRDs not upgraded between chart versions | Helm does not re-apply CRDs on upgrade by design | Manually apply new CRDs from the chart's crds/ directory, or use --force cautiously. cert-manager and similar projects document the CRD upgrade procedure. |
Viewing Helm Logs
Helm itself is stateless -- release history is stored as Kubernetes Secrets in the release's namespace (type helm.sh/release.v1). To debug a flaky install:
helm install --debug --dry-run my-release ./my-chartThis prints the rendered manifests and Helm's internal decision log. Pair it with -v 6 for verbose HTTP-level tracing against the Kubernetes API.
FAQ
Do I need Tiller?
No. Tiller was the server-side component of Helm 2 and is completely gone in Helm 3. Helm 3 is a pure client that talks directly to the Kubernetes API, stores release metadata in Secrets, and requires no cluster-side install. Every command in this guide assumes Helm 3.
Where does Helm store release history?
By default, in a Kubernetes Secret of type helm.sh/release.v1 per release, per revision, in the release's namespace. You can change the backend to ConfigMaps or SQL via the HELM_DRIVER environment variable, but Secrets are the default and recommended because values can contain sensitive data. Inspect with: kubectl -n <namespace> get secrets -l owner=helm.
How is Helm different from Kustomize?
Kustomize is a template-free patch tool: you start from a base YAML and overlay patches per environment. Helm is a template engine with packaging, versioning, repositories, and release management. The two can coexist -- you can pipe helm template output into Kustomize, or use the Helm chart inflator inside Kustomize. In practice: use Helm for third-party applications (Prometheus, PostgreSQL, ingress-nginx) where a packaged chart already exists; use Kustomize for in-house apps where you want strict declarative manifests without Go templating. Many teams use both.
Can I use Helm without the internet?
Yes. Chart tarballs are self-contained. Download a chart once with helm pull bitnami/postgresql --version 15.5.20 and commit the resulting .tgz to your infrastructure repo. Install with helm install postgres ./postgresql-15.5.20.tgz. For air-gapped clusters, mirror the charts to an internal Harbor or Chartmuseum instance.
How do I uninstall a chart cleanly?
helm uninstall <release-name> -n <namespace>This deletes every resource Helm created for that release. By default it also deletes the release history, which means you cannot roll back afterwards -- pass --keep-history if you want to preserve the audit trail. Important: Helm does not delete CRDs or PersistentVolumeClaims automatically. Those are considered data and must be removed manually: kubectl delete pvc -l app.kubernetes.io/instance=<release>.
Should I commit values.yaml to Git?
Yes, but never commit secrets. Keep values.yaml in Git; keep passwords, API keys, and TLS certificates in Kubernetes Secrets (referenced from values via existingSecret) or a dedicated secrets manager like sealed-secrets, SOPS, or HashiCorp Vault. The Helm docs on values best practices go deeper on this.
Next Steps
With Helm installed and understood, here is the natural progression for building out a real platform:
- Install cert-manager for automatic TLS -- Follow How to Install cert-manager on Ubuntu 24.04 to issue Let's Encrypt certificates to every ingress Helm deploys. Takes 10 minutes, saves you from manual certificate renewals forever.
- Set up ArgoCD for GitOps -- How to Install ArgoCD on Ubuntu 24.04 shows how to reconcile your Helm releases (or Helmfile, or Kustomize) from a Git repository automatically. Merge a PR and your cluster updates.
- Deploy the kube-prometheus-stack chart --
helm install monitoring prometheus-community/kube-prometheus-stackgives you Prometheus, Grafana, Alertmanager, and dozens of pre-built dashboards in one command. It is the reference example of what Helm enables.
- Write your second chart -- Now that you have scaffolded
dm-web, try packaging a real internal service. Use sub-charts for its database, templatize environment-specific overrides, and push the chart to an OCI registry for CI reuse.
- Read the Helm chart best practices guide -- The official best practices documentation covers naming conventions, label schemas, template composition, and versioning strategies that will save you from rewriting charts six months from now.
- Explore the Artifact Hub -- artifacthub.io indexes 15,000+ charts from hundreds of repositories. Before you write anything, check if someone already packaged it.
Need a VPS to run your Kubernetes + Helm lab?>
The CloudCore Starter plan is purpose-built for single-node k3s clusters: 4 vCPU, 8 GB RAM, 75 GB NVMe, unmetered bandwidth, from EUR 7.99/month. Root access, full kernel control, and Ubuntu 24.04 ready to go.>
Deploy in 60 seconds and follow this guide end-to-end.