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 ```console $ 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. ```yaml title="values.yaml" # 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 [running a StatefulSet with storage](/docs/hetzner/apalla/workloads/stateful/run-a-statefulset-with-storage) 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 ```console $ 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 [Argo CD](/docs/hetzner/apalla/workloads/delivery/deploy-apps-from-git-argo-cd) render the chart from Git: an upgrade then becomes a values change in a pull request.