Skip to main content

CloudNativePG's Snapshot PITR Can Restore a Multi-TB Postgres in Minutes — Hetzner's CSI Driver Still Can't Take the Snapshot

9 min readDora NodaDora Noda
Share
On this page

An EDB engineer benchmarking CloudNativePG's Volume Snapshot backup mode on AWS EKS recovered a 4.5TB Postgres database — from snapshot to accepting connections — in two minutes. Object-store recovery of a database a hundred times smaller routinely takes thirty to sixty minutes. That gap is why CloudNativePG's declarative Volume Snapshot support, shipped in v1.21 back in 2023, is the backup mode every self-hosted Postgres-on-Kubernetes operator eventually wants.

If that operator is running on Hetzner Cloud Volumes — which a lot of self-hosted PaaS builds are, bex's own reference deployments included — they can't have it. Not "it's complicated." Not "with a workaround." Hetzner's own block-storage CSI driver, hcloud-csi, has never implemented the Kubernetes CSI Snapshot API, and as of the most recent maintainer comment on the open feature request, it isn't going to gain it on any published timeline. backup.volumeSnapshot in a CNPG Cluster spec is simply not a working option against stock Hetzner Cloud Volumes today.

bex deliberately doesn't manage the database for you — an App gets deployed from a git push, but the Postgres cluster sitting next to it is the operator's own CNPG install, their own backup policy, their own restore runbook. That makes "can I actually use CNPG's fast snapshot-based PITR on the infrastructure I'm running" a real, concrete decision every Hetzner-backed operator has to make once, not a hypothetical. Here's what's actually true about the feature, why it's blocked on Hetzner specifically, and the two runbooks that do work.

What CNPG's Volume Snapshot backup actually is — and isn't, yet

CNPG has supported Kubernetes-native Volume Snapshots as a backup method since v1.21. The configuration is declarative, sitting directly in the Cluster spec:

yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: pg-primary
spec:
  instances: 3
  storage:
    storageClass: fast-snapshottable
    size: 100Gi
  walStorage:
    storageClass: fast-snapshottable
    size: 20Gi
  backup:
    volumeSnapshot:
      className: my-volumesnapshotclass
      snapshotOwnerReference: cluster
    plugins:
      - name: barman-cloud.cloudnative-pg.io
        isWALArchiver: true
        parameters:
          barmanObjectName: wal-archive
yaml
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
  name: pg-primary-snapshot
spec:
  schedule: "0 0 2 * * *"
  backupOwnerReference: self
  method: volumeSnapshot
  cluster:
    name: pg-primary

Once a VolumeSnapshotClass exists for the storage class backing PGDATA, CNPG hands the complexity of incremental and differential block-level copy to the CSI driver — the operator just requests a VolumeSnapshot object and lets the storage layer figure out how little data actually needs copying since the last one. That's the mechanism behind the two-minute, 4.5TB restore.

It's worth being precise about what this backup mode is not built on, because the two get conflated in a lot of secondary coverage: Kubernetes' VolumeGroupSnapshot API — which lets a CSI driver snapshot several PersistentVolumeClaims atomically, at the exact same instant — only reached GA in Kubernetes v1.36, in April 2026, three years after CNPG shipped its Volume Snapshot backup mode. CNPG doesn't use it yet.

When a cluster has WAL on a separate PVC from PGDATA (the walClassName option in the config above), CNPG today takes two independent VolumeSnapshot objects, one per volume, not one atomic group snapshot. Consistency between them comes from the older, cruder technique of fencing a standby before the snapshot (a "cold" backup) or coordinating an online backup around a Postgres checkpoint — not from a Kubernetes-level atomicity guarantee. CNPG's own maintainers describe the project as "following the progress" of VolumeGroupSnapshot toward eventually using it for true atomic snapshots across PGDATA, WAL, and tablespaces. That's a stated future direction, not a shipped dependency.

The Hetzner wall

None of the above matters if the underlying CSI driver can't take a snapshot in the first place, and on Hetzner Cloud Volumes, it can't. There's no VolumeSnapshotClass you can point at csi.hetzner.cloud and have it work — the driver's CreateSnapshot RPC isn't implemented, because the Hetzner Cloud API it wraps doesn't expose block-volume snapshots at all.

This isn't a stale or ambiguous gap. It's a single, still-open GitHub issue with a clear paper trail:

  • January 15, 2025 — the feature request goes up, asking for VolumeSnapshot support so Hetzner-hosted clusters can use standard CSI-based backup tooling.
  • January 22, 2025 — a Hetzner engineer replies: "VolumeSnapshots are currently not supported by the Hetzner Cloud API. I have added your feedback to our internal tracking tool."
  • August 18, 2025 — same answer, seven months later: "VolumeSnapshots are still not supported by the Hetzner Cloud API."
  • January 26, 2026 — asked directly for a status update, the same engineer writes: "No. There will also never be updates from my side on unreleased features."
  • June 12, 2026, the most recent comment on the thread — a user summarizes what everyone on the issue has independently learned: "the speed of development on hetzner is slow as nails... but on hetzner, I wouldn't hold my breath."

The issue sits at 52 upvotes and 18 comments, all pointing the same direction: no ETA, no roadmap commitment, and a maintainer response that's gotten flatter, not more encouraging, over eighteen months. Treat backup.volumeSnapshot on stock Hetzner Cloud Volumes as permanently unavailable, not pending.

What actually runs on Hetzner today: the Barman Cloud Plugin path

The backup mode that does work, and that a Hetzner-backed CNPG cluster should default to, doesn't touch the CSI layer at all. It's WAL archiving through the Barman Cloud Plugin, shipping every WAL segment to an S3-compatible object store — Hetzner's own Object Storage product, available in Falkenstein, Helsinki, and Nuremberg, works directly:

yaml
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
  name: wal-archive
spec:
  configuration:
    destinationPath: "s3://pg-backups/pg-primary"
    endpointURL: "https://fsn1.your-objectstorage.com"
    s3Credentials:
      accessKeyId:
        name: hetzner-object-storage-creds
        key: ACCESS_KEY_ID
      secretAccessKey:
        name: hetzner-object-storage-creds
        key: SECRET_ACCESS_KEY
    wal:
      compression: gzip

Point a Cluster's backup.plugins stanza at that ObjectStore (as in the YAML earlier in this post) and CNPG starts archiving WAL continuously. The RPO figure attached to this feature — ≤5 minutes, even across regions — is real, and it's earned by a specific, boring default: CNPG sets archive_timeout to 5 minutes, so even an idle cluster force-closes and ships its current WAL segment at least that often. "Across regions" isn't a special mode; it falls out of the fact that the WAL archive is an S3 bucket, decoupled from any specific compute node or Kubernetes cluster. Restore against the same bucket from a fresh CNPG cluster in a different Hetzner location and the RPO math holds, because nothing about it depended on where the original primary was running.

Cost is close to free for a typical fleet: Hetzner Object Storage runs €4.99/month per TB, which includes 1TB of egress traffic; overage is €0.0067/TB-hour for extra storage and €1/TB for extra egress. For a WAL archive measured in tens of gigabytes, that's a rounding error against the compute bill.

What it doesn't give you is the two-minute restore. Recovering a large database this way means pulling a base backup, then replaying every WAL segment since — proportional to database size and time since the last base backup, realistically tens of minutes to a couple of hours for a multi-hundred-gigabyte cluster, not the near-instant restore Volume Snapshot delivers on a CSI driver that actually supports it.

If the restore-time gap actually matters: swap the storage layer, don't wait on Hetzner

For most self-hosted Postgres instances — the size range a typical bex-deployed app's database sits in — WAL-replay RTO is fine. It only becomes a real problem once a database is large enough that "tens of minutes to hours" turns into a genuine incident-duration risk. If that's the actual constraint, the fix isn't waiting on Hetzner's roadmap — it's running a storage layer on top of Hetzner's raw compute that implements CSI snapshots itself. Piraeus, the Kubernetes operator for LINSTOR (DRBD-based distributed block storage), is the direct answer: it ships its own CSI driver, linstor.csi.linbit.com, which fully implements CreateSnapshot, and runs on any node disks — including Hetzner Cloud Volumes attached to Hetzner VMs — regardless of what the underlying cloud API does or doesn't expose. The LINSTOR Operator has supported VolumeGroupSnapshot since its 2.10 release, which is where the Kubernetes 1.36 group-snapshot story actually pays off today — on a storage layer that adopted it, not on hcloud-csi or on CNPG's own snapshot code path.

The swap is a storage-class change, not a CNPG config rewrite:

yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: piraeus-snapshot
driver: linstor.csi.linbit.com
deletionPolicy: Delete

Point storage.storageClass and walStorage.storageClass at a Piraeus-backed StorageClass, set backup.volumeSnapshot.className to piraeus-snapshot, and the same ScheduledBackup YAML from the first section works — CNPG doesn't know or care that the CSI driver underneath changed.

What it costs: DRBD replication means every write gets synchronously mirrored across however many replicas the storage pool is configured with, which is real disk and network I/O overhead that stock Hetzner Cloud Volumes don't carry, plus an entire operator (Piraeus's controller and per-node satellites) to deploy, upgrade, and monitor. That's a legitimate operational tax to accept only when the restore-time gap is an actual SLA problem, not a default.

The runbook, in one table

PathStorage backendRPORestore time (multi-TB DB)Extra infraWorks on stock Hetzner Cloud Volumes?
Barman Cloud Plugin (WAL archive)Hetzner Object Storage (S3)≤5 minTens of minutes to hoursNone beyond a bucketYes
Native CNPG Volume Snapshothcloud-csiN/AN/ANone — but blockedNo, CreateSnapshot unimplemented
CNPG Volume Snapshot on Piraeus/LINSTORLINSTOR (DRBD)≤5 min (pair with WAL archive)Minutes (EDB's own gp3 benchmark: ~2 min for 4.5TB)Piraeus operator + replication overheadYes, with an added storage layer

For most operators the answer is the first row by default: it costs almost nothing, needs no extra infrastructure beyond a bucket, and delivers the documented RPO guarantee CNPG advertises. The third row is what to reach for specifically when database size makes WAL-replay RTO a real risk — not a default to build toward on day one. The one row that isn't a decision at all, on Hetzner, is the second: it's simply not available, and there's no maintainer signal that it will be.

bex is the open-source, AI-native Render alternative — push a git repo, get a running HTTPS service on machines you own. It won't decide your Postgres backup runbook for you, because it doesn't manage the database — but it'll happily run next to whichever one you pick. Star the repo on GitHub or deploy your first app today.

Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex