How to back up Heroku Postgres, and keep the copy outside Heroku
Heroku Postgres has two kinds of backup: PGBackups, which are pg_dump files
Heroku takes and keeps for you, and rollback, which makes a new database as it was at an
earlier moment. Both live with the add-on. When the add-on is removed, by a person, a script
with the API key, or an account that lapses, its backups go after a short grace period.
This page is the copy that stays: the connection string, the command, where the file goes,
the restore, and the check. The last section is how SafeGrd runs it with no server of your
own.
What Heroku keeps, by plan
From Heroku's Dev Center, read on 2026-10-09:
| Plan | Manual backups | Scheduled backups | Rollback |
|---|---|---|---|
| Essential | Last 5 | 7 daily, 1 weekly | None |
| Standard | Last 25 | 7 daily, 4 weekly | Up to 4 days |
| Premium | Last 50 | 7 daily, 8 weekly, 12 monthly | Up to 7 days |
- Backups go with the add-on. Heroku deletes the backups of a deprovisioned add-on after a grace period its docs do not state. Download anything you want to keep before then.
- PGBackups is for databases up to 20 GB that are not heavily loaded, takes one of the plan's connections while it runs, and is not offered on Heroku Postgres Advanced.
- Backups are stored in the US whatever region the database is in.
- Rollback makes a new database from the moment you choose, which you then promote. It is the fastest undo for a bad migration, on Standard and above.
1. The connection string
The app's DATABASE_URL config var holds it. Heroku updates that var when the
credential rotates, so a backup that runs outside Heroku should read it at the time it runs
rather than keep a copy:
Heroku Postgres requires TLS; add ?sslmode=require if the string does not
carry it. A database in a Private or Shield space is reachable only from inside the space,
so the backup runs on a dyno there.
2. A read-only credential (Standard and above)
The default credential owns the database and can write. On Standard, Premium, Private and Shield plans, make one for the backup:
A new credential starts with CONNECT only. As the default credential, grant it
read access (GRANT pg_read_all_data TO backup; on PostgreSQL 14 and later), then
attach it to an app to get its string as a config var. Essential plans have only the default
credential, so the backup uses that one; keep it in one secret store and give it to nothing
else.
3. Dump it
This is the format PGBackups writes, so heroku pg:backups:restore takes it too.
--no-owner --no-privileges leaves out Heroku's generated role names, which do
not exist anywhere else. Match the client to the database's major version
(heroku pg:info shows it): an older pg_dump refuses a newer
server.
To keep Heroku's own backup instead of taking a second one,
heroku pg:backups:download fetches the newest, and
heroku pg:backups:url gives a link that expires after 60 minutes.
4. Encrypt it, ship it, schedule it
A dump holds every row in the clear. Encrypt it on the way out and write it to a bucket in an account that is not Heroku's and not the application's:
A scheduled GitHub Actions workflow runs it with HEROKU_API_KEY in the
repository's secrets, after installing the Heroku CLI, which GitHub's Ubuntu runners do not
carry; the Supabase page has a 10-line
workflow to start from. set -o pipefail makes a failed dump fail the job
instead of uploading a cut-off file. The bucket policy, a key that cannot delete and the
lock are in the S3 guide and
the Object Lock guide.
5. Restore
Into Heroku: provision a new add-on and restore over its string, or upload the dump and use
heroku pg:backups:restore with a signed URL. Into anything else: create an
empty database on any PostgreSQL of the same or a newer major version.
--exit-on-error stops at the first failure instead of carrying on and
reporting success. The dump says CREATE EXTENSION for each extension the
database used; a target that lacks one fails there, so install it first.
6. Check what came back
Compare each table's row count in the restored database with the source, as How to test that a backup restores shows, and do it on a schedule.
How SafeGrd does it
SafeGrd takes the backup from a scheduled GitHub Actions job, or on a machine it starts for each backup from the connection string, and encrypts it before upload. Each backup uploads only what changed since the last one. A drill restores the newest snapshot into a throwaway PostgreSQL of the same major version and records the tables and rows that came back, and the console alerts when a backup fails or does not run. 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 The GitHub Actions setup
- run: curl -fsSL https://cli-assets.heroku.com/install.sh | sh
- run: |
url=$(heroku config:get DATABASE_URL -a example-app)
echo "::add-mask::$url"
echo "DATABASE_URL=$url" >> "$GITHUB_ENV"
env:
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
- uses: safegrd/backup-action@v0
with:
config: ${{ secrets.SAFEGRD_CONFIG }}
database-url: ${{ env.DATABASE_URL }}
The setup is the Supabase guide's, with the string read from Heroku on each run so a rotated credential does not break the backup. This page's Heroku steps have not been run against a Heroku app by SafeGrd yet; the Action and the CLI are the ones the Supabase guide runs. Back up on SafeGrd under Surfaces in the console takes a string too, and a machine SafeGrd starts for each backup dumps and encrypts it, then is destroyed (how). Replace the string there after a credential rotation.
Questions
Should I turn off PGBackups?
Keep them. Heroku's scheduled backups and rollback are the quick undo inside Heroku, and they come with the plan. The copy outside Heroku is for when the add-on, the app or the account is what was lost.
My database is over 20 GB.
Heroku points larger databases at rollback and followers in place of PGBackups. A
pg_dump of your own still works at that size; run it against a follower so the
read stays off the leader.
Related
- How to back up a PostgreSQL database: pg_dump's formats and flags, restore and the check
- Render backup: the same job on Render
- pg_dump command builder: pick the flags and get the matching restore