Skip to content

Backup and restore

Sulu ships a dockerized backup tool (sulu-backup) that snapshots Postgres + MinIO into a GPG-encrypted bundle and uploads to any S3-compatible destination (AWS S3, Cloudflare R2, Backblaze B2, Wasabi, self-hosted MinIO, etc.).

RPO / RTO

  • RPO ≈ 24 h with the default nightly schedule (SULU_BACKUP_CRON="0 3 * * *"). Sulu takes one logical snapshot per run, so the most data you can lose is the writes since the last dump. Tighten it by running more often — e.g. 0 */6 * * * for ≈ 6 h — at the cost of extra load on your database and more bundles to retain.
  • No point-in-time recovery. These are full logical dumps, not continuous WAL archiving: you restore to a snapshot boundary, not to an arbitrary instant.
  • RTO is the wall-clock of a restore, which depends on your database size and hardware. Measure it with the automated restore drill below rather than guessing.

Automated restore drill

A backup you have never restored is a hope, not a backup. Sulu can run a scheduled restore drill that downloads the newest bundle, decrypts it, restores it into a disposable throwaway database (never your production one), and checks the data is intact — then emits a success/failure metric your monitoring can alert on. Enable it alongside the backup:

  • Compose: set SULU_BACKUP_DRILL_CRON (e.g. "0 5 * * 0", weekly) and SULU_BACKUP_DRILL_GPG_PRIVATE_KEY (the decrypt half of your keypair).
  • Helm: backup.drill.enabled: true.

The drill needs the private key (to decrypt), so it imports it into an isolated, shredded keyring for the duration of the run. See the env reference below and the internal runbook for details.

Prerequisites

  1. A GPG keypair you control. Export the public key only; keep the private half offline (this is what you'll need to decrypt during restore).

    bash
    gpg --quick-generate-key "Sulu Backups <[email protected]>" rsa4096 sign,encrypt 2y
    gpg --armor --export [email protected] > sulu-backup.pub.asc
  2. A destination S3 bucket and IAM credentials with s3:PutObject, s3:ListBucket, and s3:DeleteObject permissions (the last is for retention pruning).

Compose

Add the overlay:

bash
docker compose \
  -f docker-compose.yml \
  -f docker-compose.prod.yml \
  -f docker-compose.backup.yml \
  up -d

In your .env:

bash
SULU_BACKUP_CRON="0 3 * * *"
SULU_BACKUP_S3_ENDPOINT=https://s3.eu-west-1.amazonaws.com
SULU_BACKUP_S3_BUCKET=sulu-backups
SULU_BACKUP_S3_KEY_ID=AKIA...
SULU_BACKUP_S3_KEY_SECRET=...
SULU_BACKUP_RETENTION_DAYS=30
SULU_BACKUP_GPG_PUBLIC_KEY="-----BEGIN PGP PUBLIC KEY BLOCK-----
...your ASCII-armored public key...
-----END PGP PUBLIC KEY BLOCK-----"

The backup container runs crond and fires the backup script on the configured schedule. Check progress with docker logs -f sulu-backup.

Cron format is validated

SULU_BACKUP_CRON must be a strict 5-field POSIX cron expression (minute hour day-of-month month day-of-week — only digits and * , / - allowed). The entrypoint refuses to start with an exit-1 error if the value contains anything else (including stray whitespace or newlines from a malformed .env).

Kubernetes (Helm)

In your overrides:

yaml
backup:
  enabled: true
  schedule: "0 3 * * *"
  retentionDays: 30
  destination:
    s3Endpoint: "https://s3.eu-west-1.amazonaws.com"
    s3Bucket: "sulu-backups"
    s3KeyId: "AKIA..."
    s3KeySecret: "..."
  gpgPublicKey: |
    -----BEGIN PGP PUBLIC KEY BLOCK-----
    ...
    -----END PGP PUBLIC KEY BLOCK-----

This creates a CronJob that fires per the schedule. Watch with:

bash
kubectl get cronjob -n sulu sulu-backup
kubectl logs -n sulu -l app.kubernetes.io/component=backup --tail=200

Restore one-liner

bash
# 1. Download the bundle
aws s3 cp s3://sulu-backups/<STAMP>/db.dump.gpg .

# 2. Decrypt
gpg --decrypt db.dump.gpg > db.dump

# 3. Restore into a fresh database
pg_restore -h <new-pg-host> -U sulu -d sulu \
           --clean --if-exists \
           --no-owner --no-privileges \
           -j 2 db.dump

Attachments restore is similar — tar -xzf attachments.tar.gz and mc mirror back into MinIO.

Full disaster-recovery procedure with rollback scenarios and edge cases: docs/operations/restore-runbook.md (internal — for site reliability engineers).

Sulu Test Management System