Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

examples

File Purpose
render-backup.sh Unattended logical backup with rotation, verification and an expiry countdown
restore-into.sh Restore a dump into any Postgres server, with a rollback snapshot of the target
schedule.md cron, systemd timer and GitHub Actions recipes for the backup script

Requirements

pg_dump, pg_restore and psql at version 16 or newer. Older clients cannot read newer servers' archives, and the error you get is not helpful.

pg_dump --version   # should be >= the Render server's major version

curl is optional and only used for ALERT_WEBHOOK. flock is optional; the script falls back to a directory lock without it.

Getting the connection string

Render dashboard → your Postgres instance → ConnectExternal. Use the external URL, not the internal one: the internal hostname resolves only from inside Render's network, so a backup running anywhere else will fail with a DNS error that looks like a credentials problem.

export DATABASE_URL='postgresql://user:password@host.oregon-postgres.render.com/dbname'

First run

export DB_CREATED_AT='2026-08-01'    # the date you created the instance
export BACKUP_DIR=./backups
export KEEP=14
./render-backup.sh

Output is one directory-format dump plus a schema-only .sql file per run. The schema file is small and diffable — worth keeping in a repository so you can see when a migration changed something you did not expect.

The script verifies every dump with pg_restore --list before it reports success, because a dump that cannot be listed is not a backup, and this failure is silent otherwise.

Exit codes

Code Meaning What a scheduler should do
0 Backup taken, expiry not near nothing
1 Backup failed page someone
2 Misconfigured (missing DATABASE_URL, missing tooling) fix the config
3 Backup taken, but the instance is within 7 days of expiry act on it

Code 3 exists because "the backup worked" and "everything is fine" are different statements, and a scheduler that only checks for zero will tell you everything is fine right up until the database is deleted.

Restoring

export DUMP_PATH=./backups/20260818-031700.dump
export TARGET_DSN='postgresql://freebase@HOST:5432/mydb'
./restore-into.sh

Five steps: check the archive, check the target, snapshot the target if it is not empty, restore, then ANALYZE and print row counts and sequence positions.

Restoring into a throwaway container is the only way to know the backups work. schedule.md ends with a four-minute drill; do it monthly.

Credentials

Every value comes from the environment. Keep them in a .env that is chmod 600 and covered by .gitignore — which it already is in this repository. A dump file and a live connection string in the same directory is a bad combination to accidentally push.