Guide
Back up DigitalOcean Managed Postgres to your own S3 bucket
DigitalOcean's managed backups keep about seven days, at cluster level, and you can't download them. Here is how to take a logical dump of each database to an S3-compatible bucket you control — first by hand with cron, then automatically.
What DigitalOcean's built-in backups do and don't give you
DigitalOcean Managed Databases take automatic daily backups with point-in-time recovery. What that doesn't cover:
- Retention is fixed at roughly seven days — no long-term monthly archive.
- Restore is cluster-only: you recover into a brand-new cluster, not one database into the existing one. (See restoring a single database.)
- You can't download the backup. It lives inside DigitalOcean; if you leave, it doesn't come with you.
For a portable, long-lived, per-database archive you own, you take your own logical dumps.
Step 1 — create a least-privilege backup role
Don't dump as doadmin. Create a read-only role that can connect and read, nothing more:
-- run once, as doadmin, on each database you back up
CREATE ROLE dbferry_backup LOGIN PASSWORD '…';
GRANT CONNECT ON DATABASE client_b TO dbferry_backup;
GRANT USAGE ON SCHEMA public TO dbferry_backup;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO dbferry_backup;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO dbferry_backup;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT ON TABLES TO dbferry_backup;
Step 2 — dump one database, compressed, over TLS
Managed connections require TLS (sslmode=require). Use the custom format so you can restore selectively later:
pg_dump --format=custom --no-owner --no-privileges \
"postgresql://dbferry_backup:PASS@db-x.db.ondigitalocean.com:25060/client_b?sslmode=require" \
| zstd -19 > client_b.dump.zst
Step 3 — push it to your S3 bucket
Any S3-compatible storage works — AWS S3, DigitalOcean Spaces, Cloudflare R2, Backblaze B2, Wasabi. With Spaces:
aws s3 cp client_b.dump.zst \
s3://my-backups/client_b/$(date +%Y/%m/%d-%H%M).dump.zst \
--endpoint-url https://fra1.digitaloceanspaces.com
Step 4 — automate with cron (and encrypt)
Wrap it in a script and schedule it. Since the dump leaves your machine and lands in object storage, encrypt it before it goes — client-side, with a key the storage provider never sees. age is a good fit:
#!/usr/bin/env bash
# /opt/backup/client_b.sh — one line in crontab: 0 3 * * *
set -euo pipefail
ts=$(date +%Y/%m/%d-%H%M)
pg_dump --format=custom "$DSN" \
| zstd -19 \
| age -r "$AGE_RECIPIENT" \
| aws s3 cp - "s3://my-backups/client_b/$ts.dump.zst.age" --endpoint-url "$SPACES"
That pipeline never writes the dump to local disk — it streams dump → compress → encrypt → upload in one pass. Repeat the block per database, add log capture and alerting, and you have a real backup job.
Or let dbferry run it
dbferry is exactly this pipeline — pg_dump | zstd | age | S3 multipart — as a scheduled service against your DigitalOcean cluster, backing up each database separately into your own bucket, with retention, failure alerts, and a dead-man's switch. Client-side encryption, zero data retention: the age key stays yours.