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](/docs/hetzner/apalla/storage/choose-storage), 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. ```yaml title="writer-statefulset.yaml" 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](/docs/hetzner/apalla/storage/block/attach-constraints-and-reclaim). 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: ```yaml title="app-deployment.yaml" 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](/docs/hetzner/apalla/storage/access-modes) 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](/docs/hetzner/apalla/storage/choose-storage) weighs single-writer block storage against replicated local disks. For the data this workload holds, plan [Disaster recovery for stateful workloads](/docs/hetzner/apalla/storage/backup/disaster-recovery-stateful) next.