Use Hetzner Cloud volumes
Hetzner Cloud volumes are the zero-setup default for a single-writer app on cloud nodes. The standard StorageClass is already in your cluster, backed by the hcloud CSI driver, so you create a PVC and mount it without installing anything.
Confirm the driver is running #
The hcloud CSI driver ships built in and Syself operates it. Confirm it is registered and the class is present:
$ kubectl get csidrivers csi.hetzner.cloud
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
csi.hetzner.cloud true true false <unset> false Persistent 12d
$ kubectl get storageclass standard
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) csi.hetzner.cloud Retain WaitForFirstConsumer true 12d
The standard class is ReadWriteOnce, binds on first use, and keeps data on delete. Each volume sits on Ceph with three copies, so a failed disk does not lose the data. Hetzner caps a single server at 16 attached volumes. ReadWriteOnce is per node: pods on the same node can share one volume, but pods on different nodes need shared storage . See the storage classes reference for the full properties.
Note
The standard class only works on cloud nodes. A pod that requests it on a bare metal node stays Pending, because the hcloud volume cannot attach there. See Volume attach constraints and reclaim policies .
Create a PVC and mount it #
This manifest creates a 10Gi claim and a pod that mounts it. The node affinity keeps the pod off bare metal, where the volume cannot attach.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pv-claim
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
name: pv-pod
spec:
containers:
- name: app
image: nginx
volumeMounts:
- mountPath: /usr/share/nginx/html
name: pv-storage
volumes:
- name: pv-storage
persistentVolumeClaim:
claimName: pv-claim
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance.hetzner.cloud/is-root-server
operator: NotIn
values:
- "true"
Apply it:
$ kubectl apply -f pv-demo.yaml
Watch it bind #
Look at the PVC right after applying, and it reads Pending:
$ kubectl get pvc pv-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
pv-claim Pending standard <unset> 5s
That is WaitForFirstConsumer doing its job. Once the pod lands on a node, the driver creates the volume and the PVC flips to Bound:
$ kubectl get pvc pv-claim
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
pv-claim Bound pvc-8f3c... 10Gi RWO standard <unset> 30s
Verify the mount #
Write a file through the pod, then confirm it is there:
$ kubectl exec -it pv-pod -- sh -c 'echo "stored on a Hetzner volume" > /usr/share/nginx/html/hi.txt && cat /usr/share/nginx/html/hi.txt'
stored on a Hetzner volume
Anything written under /usr/share/nginx/html now lives on the volume and survives the pod. Because the class reclaim policy is Retain, deleting the PVC keeps the Hetzner volume, so a mistaken delete cannot wipe your data. The flip side is that the volume keeps costing money until you delete it yourself, as a separate and intentional step.
Waiting for the pod also fixes placement. A Hetzner Cloud volume can only attach to a node in the same Hetzner location. By holding until the pod is scheduled, the driver creates the volume in that same location, so it can always attach. A cluster spread across locations still works, because each volume is born next to its pod.
Related: Expand a volume , Run a single-writer workload on block storage .
Choose storage for a workload
Pick the right storage on Syself Autopilot from two questions, how many writers and what happens to the data when a node is replaced.
Expand a Hetzner Cloud volume
Grow a Hetzner Cloud volume in place by editing the PVC size, and understand why a volume can only grow, never shrink.