Skip to main content

Deploy an application with Helm

Components like ingress-nginx, Prometheus, and cert-manager ship as Helm charts, as does most off-the-shelf software you will run on a cluster. Helm installs a chart, a packaged application, as a single named release you can upgrade and roll back as a unit. Every chart ships with default values; you override those defaults in a values file to fit a Syself Autopilot cluster.

Install with values 

				$ helm repo add example https://charts.example.com
$ helm install my-app example/some-app \
  --namespace team-web --create-namespace \
  --version 3.2.1 \
  --values values.yaml
			

On Syself Autopilot, two settings in the values file usually need adjusting regardless of the chart: the storage class its volumes bind to, and the location of any load balancer it creates.

values.yamlyaml
				# Bind persistent volumes to a storage class that actually exists on the cluster
persistence:
  storageClass: local-nvme # local NVMe; or a Hetzner Cloud volume class
# If the chart creates its own LoadBalancer Service, tell Hetzner where to place it
service:
  annotations:
    load-balancer.hetzner.cloud/location: fsn1
			

Getting either wrong is the most common reason a chart that works elsewhere fails to start on Syself Autopilot.

Helm on this platform 

Most charts are written to land on a cluster with a single default StorageClass, where a PersistentVolumeClaim that names no class just works. Syself Autopilot hands you one, standard, backed by Hetzner Cloud volumes. It is still worth setting the class per application, because the right storage depends on the workload: local NVMe through local-nvme is fast but pinned to one node, while a Hetzner Cloud volume class is network-attached and follows the pod when a node is replaced. That choice is yours to make, per application: see for which fits what.

The practical consequence: a chart whose PVC has no storageClassName set, and no persistence.storageClass value passed, lands on standard. Its volume stays Pending until a pod schedules, because standard binds on first consumer, then binds where that pod runs. Setting persistence.storageClass (or whatever key the chart exposes) is what puts it somewhere else. When pods hang on a fresh install, a kubectl describe pvc in the namespace usually names the reason directly.

The load balancer annotation is the same idea for networking. A chart that provisions its own Service of type LoadBalancer gets a Hetzner load balancer, and load-balancer.hetzner.cloud/location decides which location it lives in. Many charts already expose a service.annotations map for exactly this; pass it there rather than editing the rendered Service after the fact.

Pin versions 

Pin the chart version with --version, and pin the application image tag through values. Those are two separate things: a chart version selects the templates and defaults, the image tag selects the code that actually runs. If you leave either unpinned, the next helm install or helm upgrade can pull something newer than you tested, shifting behavior without warning. Pinning both makes a release reproducible, which is what you want when the same values file deploys to more than one cluster.

Upgrade and roll back 

				$ helm upgrade my-app example/some-app --namespace team-web --version 3.3.0 --values values.yaml
$ helm history my-app --namespace team-web
$ helm rollback my-app 1 --namespace team-web   # back to revision 1 if the upgrade misbehaves
			

Helm records every release as a numbered revision, so a bad upgrade is one rollback away. That safety net is why even a one-manifest app is worth installing as a release rather than a raw kubectl apply.

For anything you deploy more than once, stop running helm install from a laptop and let render the chart from Git: an upgrade then becomes a values change in a pull request.