Self-host Postgres with verified backups
A backup you have never restored is a hope. This guide creates a Postgres database on your own server, schedules backups to an S3-compatible bucket, and rehearses a restore without touching live data. It assumes a running instance and a configured levelrail-cli; see getting started.
1. Create the database
levelrail-cli databases create --name main --engine postgres --version 16Other engines work the same way (redis, mysql, mongodb, mariadb, keydb, dragonfly, clickhouse). To connect an app to it, see connecting apps to databases.
2. Connect a backup target
A backup target is a separate resource that any number of databases and volumes can use. Credentials are write-only: they go in at creation and are never returned.
levelrail-cli backup-targets create \
--name primary-r2 \
--provider r2 \
--endpoint YOUR_ENDPOINT \
--bucket levelrail-backups \
--access-key-id YOUR_KEY_ID \
--secret-access-key YOUR_SECRET--provider accepts aws, r2, or custom. --endpoint is required for r2 and custom. Then test it:
levelrail-cli backup-targets test TARGET_IDThe test calls HeadBucket. It proves the bucket is reachable with those credentials, not that the key can write or delete. The first scheduled backup is the real proof.
3. Schedule backups
levelrail-cli backups schedule set main \
--target TARGET_ID \
--cron "0 3 * * *" \
--retain 7 \
--retain-days 30The cron expression is validated when you submit it. Retention has two dimensions that combine: keep at most the newest --retain successful backups, and never keep any older than --retain-days. A backup is deleted when either rule would remove it. Zero turns a dimension off.
App volumes use the same target and retention model:
levelrail-cli app-volume-backups schedule set my-app uploads --target TARGET_ID --cron "0 4 * * *" --retain 144. Check that backups verify themselves
Every scheduled backup that succeeds is downloaded and re-hashed afterwards, and the history shows whether it passed. Manual backups are not verified automatically. Trigger one by hand and read the history:
levelrail-cli backups trigger main
levelrail-cli backups list main5. Rehearse a restore
Restoring as new builds a separate database from a backup and never touches the original:
levelrail-cli backups restore-as-new main --backup BACKUP_ID --new-name main-rehearsalConnect to main-rehearsal, run a query you can check, then delete the copy. Do this on a calendar, not once.
Restoring in place overwrites the live database, is destructive, and needs you to type the database name:
levelrail-cli backups restore main --backup BACKUP_ID --confirm main6. Get told when backups stop
A backup_missing alert rule fires when the newest successful backup trails the schedule by more than the rule's grace period (default 6 hours). It catches both a schedule that stopped running and repeated failures. Route it to a channel that does not depend on the server being backed up. See observability.
What this does not cover
- Point-in-time restore for Postgres is separate (
levelrail-cli pitr) and only recovers moments after you enable it. See managing databases. - The control plane's own database needs its own backup, plus an off-server copy of the master key. See control plane backup and disaster recovery.
- Bind mounts are not backed up, only Docker-managed named volumes.