Run a single-writer workload on block storage
A rolling Deployment update on a ReadWriteOnce (RWO) volume deadlocks as soon as the new pod lands on a different node: the old pod still holds the volume, so the new pod cannot attach and never starts. ReadWriteOnce means the volume mounts on one node at a time, so only pods on that same node can share it. When pods on different nodes need the same files, you need shared storage (RWX) or replication in the app, not one block volume. A StatefulSet or a Deployment with strategy: Recreate gets around this, because both retire the old pod before the new one claims the volume.
This page is for a single-writer application that is not a database, or a dev or staging instance kept simple. A production database belongs on local NVMe under an operator , which handles replication and backups for you, not on a single block volume behind one pod.
The default Deployment strategy is RollingUpdate: it starts a new pod before stopping the old one, to keep the app available during a change. On an RWO volume that ordering is fatal. The old pod keeps the volume attached to its node, the new pod schedules elsewhere and waits to attach, and neither side gives way. The rollout hangs until you delete the old pod by hand.
A single writer cannot be handed over while it is still held. The fix is to stop the old pod first, accept a short outage, then start the new one.
Use a StatefulSet#
A StatefulSet is built for this. It replaces pods one at a time, terminating the old pod before creating its replacement, so the volume is free when the new pod attaches. Its volumeClaimTemplates also gives each replica its own PersistentVolumeClaim (PVC), the object a pod uses to claim a disk, with no shared handle to fight over.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: writer
spec:
serviceName: writer
replicas: 1
selector:
matchLabels:
app: writer
template:
metadata:
labels:
app: writer
spec:
containers:
- name: writer
image: my-app:1.0
ports:
- containerPort: 8080
volumeMounts:
- name: data
mountPath: /data
readinessProbe:
tcpSocket:
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance.hetzner.cloud/is-root-server
operator: NotIn
values:
- "true"
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
The node affinity keeps the pod on cloud nodes, where a Hetzner Cloud volume can attach. Bare metal and control-plane nodes cannot hold one. See Volume attach constraints and reclaim policies .
The readiness probe matters here. It holds traffic back until the app has opened its port on the reattached volume, so clients do not hit a pod that is still mounting its disk.
Or a Recreate Deployment#
When you want a Deployment, set strategy: Recreate. It stops every old pod before starting any new one, which sidesteps the RWO deadlock at the cost of a brief downtime during each rollout.
Unlike a StatefulSet, a Deployment has no volumeClaimTemplates, so you create the PVC yourself and reference it by name. It needs the same node affinity too, or the pod can land on bare metal where the volume cannot attach:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
containers:
- name: app
image: my-app:1.0
volumeMounts:
- name: data
mountPath: /data
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: instance.hetzner.cloud/is-root-server
operator: NotIn
values:
- "true"
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data
Plan for the reattach gap#
Syself Autopilot reprovisions nodes during upgrades, scaling, and self-healing. When the node under your pod is drained, the volume detaches and reattaches to the pod's new node, so the data follows the workload, but there is a short gap while that happens. A single writer means the app is briefly unavailable during the handoff. This is expected, not a failure.
Because the volume already exists, it pins the pod to its Hetzner location through a volume node affinity, so the pod can only start where the volume is. See Access modes and volume binding for why the existing volume leads the pod.
Important
A StatefulSet on block storage gives you one copy of the data behind one pod. It survives node replacement, but a drain still causes a short outage while the volume reattaches. This is fine for an app that tolerates brief downtime, not for a service that must stay up through every node event.
When you need to survive a node event with no outage, one volume behind one pod is not enough. Run replication at the application level instead, with several replicas each holding their own copy, so another replica keeps serving while one reattaches. That is a different storage decision: Choose storage for a workload weighs single-writer block storage against replicated local disks.
For the data this workload holds, plan Disaster recovery for stateful workloads next.
Volume attach constraints and reclaim policies
Cloud volumes attach to one cloud node at a time and are kept, not deleted, when the PVC goes away, so clean them up to stop paying.
Set up local NVMe with TopoLVM
Install cert-manager and TopoLVM, prepare each bare metal server's data disks with LVM, and get the local-nvme, local-ssd, and local-hdd storage classes.