Supabase Backup
Dump a Supabase or Postgres database to your own S3, R2 or B2 bucket, for example right before a deploy runs migrations
Dumps a Postgres database with pg_dump and uploads it to a bucket you own: AWS S3, Cloudflare R2, Backblaze B2, MinIO or anything else that speaks the S3 API. Next to the dump it writes a manifest.json (tables, estimated row counts, sizes, SHA-256 checksums) and a RECOVERY.md with restore steps. For Supabase projects it can copy Storage files too.
The step runs the backupdrill CLI, an open-source Supabase backup tool. The dump goes from your database through the Woodpecker agent straight to your bucket.
Back up before a deploy
Put the step in front of the one that runs your migrations. Steps run in order, so if the backup fails, the deploy doesn't start.
steps:
- name: backup-database
image: ghcr.io/backupdrill/woodpecker-backupdrill:1
settings:
database_url:
from_secret: database_url
s3_endpoint: https://<account-id>.r2.cloudflarestorage.com
s3_bucket: my-db-backups
s3_access_key_id:
from_secret: backup_access_key_id
s3_secret_access_key:
from_secret: backup_secret_access_key
when:
- event: push
branch: main
- name: migrate
image: your-deploy-image
commands:
- ./migrate.sh
when:
- event: push
branch: main
For Supabase, use the Session pooler connection string: open the project in the dashboard, click Connect and pick Session pooler. The default postgres user works.
For AWS S3, leave out s3_endpoint and set s3_region (for example us-east-1).
To back up on a schedule instead, create a cron job in the repository settings and run the step with when: event: cron. Each backup reads the whole database, which counts as egress on a Supabase bill, so read the egress notes in the CLI docs before you schedule it daily.
Settings
| Setting | Default | Description |
|---|---|---|
database_url |
required | Postgres connection string. Pass it with from_secret. |
s3_bucket |
required | Bucket the backup is written to. |
s3_access_key_id |
required | Access key for that bucket. Pass it with from_secret. |
s3_secret_access_key |
required | Secret key for that bucket. Pass it with from_secret. |
s3_endpoint |
none | Endpoint of an S3-compatible service (R2, B2, MinIO). Leave it out for AWS S3. |
s3_region |
auto |
Bucket region. AWS S3 needs a real region. |
s3_prefix |
backupdrill |
Key prefix inside the bucket. |
s3_force_path_style |
true when s3_endpoint is set |
Path-style addressing (true or false). |
project_name |
supabase |
Name used in the object keys and the manifest. |
schemas |
public |
Schemas to dump, as a list or comma-separated. |
supabase_storage_endpoint, supabase_storage_region, supabase_storage_access_key_id, supabase_storage_secret_access_key |
none | Set all four to copy Supabase Storage files as well. Create the keys under Storage > Settings > S3 Access Keys and pass them with from_secret. |
supabase_storage_buckets |
all buckets | Storage buckets to copy, as a list or comma-separated. |
ping_url |
none | A Healthchecks.io ping URL. The CLI pings it when the backup starts, succeeds or fails. |
The settings map one to one onto the CLI's BACKUPDRILL_* environment variables. The plugin ignores those variables (and DATABASE_URL) if they are set any other way, so only the step's settings decide what gets backed up.
A backup ends up here:
<s3_prefix>/<project_name>/<timestamp>/
dump.pgcustom pg_dump custom format
storage/<bucket>/<key> only with the Storage settings
RECOVERY.md
manifest.json written last, so a snapshot without it is incomplete
PostgreSQL versions
The image has pg_dump 16, 17 and 18. Before the backup, the plugin asks the server for its version and runs the pg_dump of the same major version; PostgreSQL 15 and older get pg_dump 16. If the version can't be read, the step fails before anything is dumped. A newer pg_dump would also work for the backup, but its dump might not restore onto your server: a dump needs a pg_restore at least as new as the pg_dump that wrote it, and pg_restore 17 and newer can't restore into PostgreSQL 16 or older.
pg_dump 16 dumps servers back to PostgreSQL 9.2, and Supabase runs 15 to 17. RECOVERY.md in each snapshot names the pg_restore version the dump needs.
A server newer than 18 stops the CLI with an error. For one of those, run the CLI in a regular step from an image that has Node.js 20 or newer and a pg_dump at least as new as the server, and set BACKUPDRILL_PG_DUMP to that pg_dump:
steps:
- name: backup-database
image: <image with Node.js and a new enough pg_dump>
environment:
BACKUPDRILL_DATABASE_URL:
from_secret: database_url
BACKUPDRILL_S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
BACKUPDRILL_S3_BUCKET: my-db-backups
BACKUPDRILL_S3_ACCESS_KEY_ID:
from_secret: backup_access_key_id
BACKUPDRILL_S3_SECRET_ACCESS_KEY:
from_secret: backup_secret_access_key
BACKUPDRILL_PG_DUMP: /path/to/pg_dump
commands:
- npx --yes backupdrill@2.7.1 backup
Which CLI version runs
The image has Node.js 24, pg_dump 16 to 18 and backupdrill 2.7.1. The CLI and all of its dependencies are installed from a lockfile when the image is built, so nothing is downloaded during a run. The agent only needs to reach your database, your bucket and, if you set ping_url, Healthchecks.
The CLI doesn't call any BackupDrill server, and a new CLI release doesn't change an image you already use. A rebuild with a newer CLI gets a new version tag: :1 moves to it, :1.0.0 stays on this build. For a different CLI version right away, run it in your own step with npx backupdrill@<version> backup.
Secrets
The entrypoint doesn't echo any setting. The CLI logs the bucket and key path it writes to, but not the connection string or the keys, and it hands the database password to pg_dump through the environment rather than the command line. Woodpecker also masks secret values in the log.
Anyone who can edit the pipeline file can change the other settings as well, for example point s3_endpoint at a server they control and receive the dump. Restricting the secrets to this image with the plugins filter doesn't prevent that. Only make the secrets available to branches and events you trust, and keep them away from pull requests (Woodpecker withholds secrets from pull request events by default).
Restoring
The plugin only takes backups. To restore one, or to check that it restores at all, use the CLI from your own machine: backupdrill drill restores the latest snapshot into a throwaway Postgres in Docker and checks it against the manifest, and backupdrill restore puts a snapshot into a new Supabase project. Each snapshot's RECOVERY.md also has the plain pg_restore commands.