Guides · Databases
— Guide
Automate Postgres and MySQL dumps to S3 or R2 with retention
~8 min read · Aug 2026 · Commands match docs v0.0.10
Nightly database dumps are a boring problem until you need one. This guide shows a practical pattern: dump on the server, push to an S3-compatible bucket with tags, prune old copies automatically, and pull the latest dump onto your laptop when you want a local restore. Credentials stay on your machines — TagBackup talks only to your bucket.
TagBackup is a small MIT-licensed Go CLI (with an optional local web UI via tagbackup serve). Tags live in the object key itself, so you can filter dumps with simple expressions instead of inventing folder trees or maintaining a side index.
When to use this
- You want one file per dump (SQL dump, compressed archive, or similar) offsite every night.
- You care about retention by tag (for example keep 30 days of
nightly+prod). - You are happy with S3-compatible object storage: AWS S3, Cloudflare R2, Backblaze B2, MinIO, and similar.
- Cron (or systemd timers) on the database host is enough; you do not need a full backup appliance.
When not to use this
- You need encrypted, deduplicated filesystem or VM snapshots — look at restic (or similar) instead.
- You need continuous directory sync between machines — rclone is a better fit.
- You expect AWS S3 object tags (metadata API). TagBackup embeds tags in the key; it does not use the S3 tagging API.
For a broader comparison, see restic vs rclone vs TagBackup for database and file dumps.
Install TagBackup
On Linux or macOS:
curl -sfL https://tagbackup.com/install.sh | sh
Or download a release binary from GitHub Releases. Confirm with tagbackup --version. Full install notes are in the docs.
Add a bucket (once)
Configure an alias that maps to your bucket. Interactive setup is fine for a first machine:
tagbackup bucket add
You will be prompted for alias, bucket name, endpoint, region, optional prefix, path-style options, and credential type (static, profile, or iam).
For Cloudflare R2 or Backblaze B2, use the S3-compatible endpoint and keys from that provider’s console. For AWS, a typical endpoint looks like https://s3.eu-west-1.amazonaws.com. R2 and B2 are often cheaper offsite homes for dumps than always-hot S3; TagBackup does not care which you pick as long as the API is S3-compatible.
Unattended / Ansible-friendly example (values are placeholders):
tagbackup bucket add \
--alias=dbbackup \
--bucket=my-company-db-backups \
--endpoint=https://s3.eu-west-1.amazonaws.com \
--region=eu-west-1 \
--credential-type=static \
--access-key-id=AKIAEXAMPLE \
--secret-access-key=wJalr...
Then verify permissions:
tagbackup bucket verify --bucket=dbbackup
Config lives locally (~/.config/tagbackup/config.yaml on Linux/macOS, mode 0600). Nothing is uploaded to tagbackup.com. For cron on servers, prefer IAM roles or environment variables (TAGBACKUP_BUCKET_<ALIAS>_ACCESS_KEY_ID / _SECRET_ACCESS_KEY) so static secrets need not sit in the YAML file — see Credentials in the docs.
Push a Postgres dump from stdin
Pipe pg_dump straight into TagBackup. Use - as the path and set --filename so the object key gets a sensible name:
pg_dump mydb | tagbackup push - \
--bucket=dbbackup \
--tag=nightly,prod,postgres \
--filename=mydb.sql
Tags are alphanumeric ([a-zA-Z0-9]), comma-separated on push, and sorted before they are written into the key. Order does not matter: nightly,prod and prod,nightly become the same tag set.
You can also dump to a file first (useful if you want gzip on disk before upload):
pg_dump mydb | gzip -c > /tmp/mydb.sql.gz
tagbackup push /tmp/mydb.sql.gz \
--bucket=dbbackup \
--tag=nightly,prod,postgres
rm -f /tmp/mydb.sql.gz
Push a MySQL dump
Same pattern with mysqldump:
mysqldump --single-transaction --routines --triggers mydb \
| tagbackup push - \
--bucket=dbbackup \
--tag=nightly,prod,mysql \
--filename=mydb.sql
Or via a temporary file if your dump is large and you prefer an explicit checkpoint before upload.
Cron: push then prune
Classic loop: push tonight’s dump, then delete dumps matching the same tag expression that are older than your retention window.
#!/usr/bin/env bash
set -euo pipefail
# Push
pg_dump mydb | tagbackup push - \
--bucket=dbbackup \
--tag=nightly,prod,postgres \
--filename=mydb.sql \
--non-interactive
# Retention: keep roughly the last 30 days of nightly+prod dumps
tagbackup delete \
--bucket=dbbackup \
--tag=nightly+prod \
--older-than=30d \
--force \
--non-interactive
--older-than accepts durations such as 30d, 2w, or 12h. Use --dry-run once before enabling --force in cron so you can see what would go.
Example crontab entry (02:15 every night):
15 2 * * * /usr/local/bin/backup-mydb.sh >> /var/log/backup-mydb.log 2>&1
On push, tags are a comma list. On delete/pull/files, --tag is an expression: + means AND, | means OR, - means NOT. So nightly+prod means “has both tags”.
Pull the latest dump onto a laptop
On a machine that has the same bucket alias configured (read-only credentials are enough):
tagbackup pull --bucket=dbbackup --tag=nightly+prod --latest
Restore into Postgres:
tagbackup pull --bucket=dbbackup --tag=nightly+prod+postgres --latest --output=- \
| psql mydb
Or list what is there before deciding:
tagbackup files --bucket=dbbackup --tag=nightly+prod
For interactive picking of an older dump, omit --latest and choose from the list. In scripts, always pair --latest with --non-interactive; no match exits with code 5.
Optional: browse with the local UI
If you prefer clicking over typing:
tagbackup serve
This starts a UI on 127.0.0.1 only (default port 3000), using your local config. You can filter and download specific rows; for scripted “give me the newest matching tag set”, stick with tagbackup pull --latest.
A note on object keys
Tags are baked into the S3 key (not AWS object-tag metadata). With a prefix, keys look like:
nightly/1776831788343-nightly,prod-dump.sql
TagBackup only manages objects that match its key pattern; other keys in the same bucket are ignored. That keeps dumps discoverable with files / pull / delete without a separate database.
Checklist
- Install TagBackup and run
tagbackup bucket add(thenbucket verify). - Pipe
pg_dumpormysqldumpintotagbackup push - … --filename=…. - Cron: push, then
delete --tag=… --older-than=30d --force. - On the laptop:
pull --tag=… --latest(orserveto browse).
Further reading
- Website: tagbackup.com
- Documentation: tagbackup.com/docs
- Source: github.com/joncombe/tagbackup
If this matches how you already think about dumps — cron out, laptop in — TagBackup is meant to stay out of the way and do exactly that.