SafeGrd / Hetzner Postgres backup

How to back up PostgreSQL on a Hetzner server to Hetzner Object Storage, locked

A PostgreSQL server on a Hetzner Cloud server or dedicated box has no backup unless you make one. Hetzner's server snapshots copy the disk, which is a copy of the same machine, not of the database at a consistent moment, and they sit in the same project as the server. Hetzner Object Storage is S3-compatible, in the same regions, and supports Object Lock, so a dump written there can be made undeletable until a date, for everyone with the project's keys. This page is that setup: the bucket, credentials that can write and not delete, the dump, the restore and the check. The last section is how SafeGrd runs it on the same server.

What you get from Hetzner, and what you have to do

From Hetzner's documentation, read on 2026-10-08:

1. A bucket with Object Lock, and a key that cannot delete

In the Hetzner Console, under Object Storage, create a bucket with Object Lock enabled, in the location of your choice, then create S3 credentials. Set a default retention on the bucket with the AWS CLI, pointed at Hetzner's endpoint, so every object written is locked without the backup script asking:

aws --endpoint-url https://fsn1.your-objectstorage.com --region fsn1 s3api put-object-lock-configuration \
  --bucket acme-db-backups \
  --object-lock-configuration 'ObjectLockEnabled=Enabled,Rule={DefaultRetention={Mode=COMPLIANCE,Days=30}}'

Hetzner's S3 credentials are valid for every bucket in the project, so the credential on the database server can delete whatever the project holds that is not locked. The lock is what protects the backups; keep the credential off any other bucket's data by giving each purpose its own project. Compliance mode means a backup written by mistake also stays for 30 days, which is the point: what a lock does and does not protect.

2. A read-only role

CREATE ROLE backup LOGIN PASSWORD 'a long random password';
GRANT pg_read_all_data TO backup;

On PostgreSQL 13, which has no pg_read_all_data, grant each schema and set the default for new tables; the PostgreSQL page has the lines. A table with row-level security needs BYPASSRLS on this role or it dumps only the rows the policies show it.

3. The backup script

#!/usr/bin/env bash
set -euo pipefail
export AWS_ENDPOINT_URL=https://fsn1.your-objectstorage.com AWS_DEFAULT_REGION=fsn1
KEY="app/$(date -u +%Y/%m/%d/app-%Y%m%dT%H%M%SZ).dump.age"
pg_dump --format=custom --no-owner "postgres://backup@localhost:5432/app" \
  | age -r "$AGE_RECIPIENT" \
  | aws s3 cp - "s3://acme-db-backups/$KEY"
echo "uploaded $KEY"

Schedule it with cron and add a check that expects a new object every day, since cron reports nothing when a job fails or stops being scheduled. The S3 guide has the cron line, the heartbeat, and the large-upload flag.

4. Restore, and check it

createdb app_restored
aws s3 cp "s3://acme-db-backups/app/2026/10/08/app-20261008T021500Z.dump.age" - \
  | age -d -i backup-key.txt \
  | pg_restore --exit-on-error --no-owner --dbname app_restored

On a new server, or the same one, with PostgreSQL at the same or a newer major version. Then compare row counts per table with the source, as How to test that a backup restores shows, and do it on a schedule.

How SafeGrd does it

SafeGrd's daemon runs on the Hetzner server, from the install script or the ghcr.io/safegrd/cli image, and writes to your Hetzner Object Storage bucket under Object Lock, with a credential that can write and never delete. It alerts when a backup fails or does not run, which cron does not. SafeGrd does the same job on every platform; the PostgreSQL page has what it dumps, how it encrypts, and what a drill checks.

Run your first Fire Drill Read the install guide

curl -fsSL https://safegrd.dev/install.sh | sh
safegrd enroll
# or standalone, with no SafeGrd account: the bucket is the only destination
safegrd init --storage s3 --s3-bucket acme-db-backups --s3-endpoint https://fsn1.your-objectstorage.com --s3-region fsn1 --retention-days 30
safegrd backup --database-url "postgres://backup:...@localhost:5432/app"

safegrd init asks the bucket whether Object Lock is on and warns when it cannot confirm it, and the first safegrd backup refuses a bucket without the lock unless the config says worm_mode: "NONE", so a bucket made without the lock is found out at the first backup rather than at the first deletion. safegrd doctor checks the same, plus the role's permissions, on an enrolled host (Storage and retention).

Questions

Why not Hetzner's server backups?

Keep them: a server backup restores the whole machine in minutes, which is the fastest way back after a disk failure. It is not a database backup. The files are copied whenever the schedule says, mid-write, and PostgreSQL recovers from that as from a crash. It restores only onto a Hetzner server of that image, it is in the same project as the server, and nothing in it is locked. The dump restores on any PostgreSQL of the same or a newer version, and the lock keeps it for 30 days.

A second region?

A bucket in hel1 for a server in fsn1 is a different data centre in a different country with the same credentials and the same lock. For a copy outside Hetzner altogether, safegrd export copies every snapshot to another bucket, still encrypted and locked, whenever you want.

Related