Guides · Workflows

— Guide

How I back up a solo project’s database every night

~6 min read · Aug 2026 · Commands match docs v0.0.10

I run a few small personal projects. Nothing fancy — one Postgres database each, a cheap VPS, and the quiet dread that one day the disk will die and I will have been “meaning to set up backups” for months.

I wanted offsite dumps without handing bucket credentials to a hosted middleman. I briefly sketched TagBackup as a micro-SaaS. Then reality intervened: no CISO (and, honestly, no sensible solo developer either) wants a random third party holding the keys to their backups. So TagBackup stayed what it is now — an MIT-licensed Go CLI on tagbackup.com. Credentials live on my machines. The binary talks only to my bucket.

This is the routine I actually use.

What “good enough” looks like for one person

I am not running a bank. I do not need continuous WAL shipping or a multi-region DR drill every quarter. For a solo project, good enough means:

  1. A fresh SQL dump lands offsite every night — automatically, via cron.
  2. I keep about a month of them so a bad migration last Tuesday is still recoverable.
  3. From my laptop I can pull the newest good dump with one command when something goes wrong.
  4. Tags mean something to me (nightly, prod) without inventing a folder tree or maintaining a side index.

TagBackup embeds those tags in the S3 object key (not AWS S3 object-tag metadata). Same word; different mechanism. It works with AWS S3, Cloudflare R2, Backblaze B2, MinIO, or anything else that speaks the S3 API.

If you need encrypted, deduplicated filesystem snapshots, look at restic instead. If you need directory sync, look at rclone. For “one dump file per night, tagged and prunable,” this pattern is enough.

Once: install and add a bucket

On the server (and later on the laptop):

curl -sfL https://tagbackup.com/install.sh | sh
tagbackup --version

Add a bucket alias interactively:

tagbackup bucket add

You will be prompted for alias, bucket name, endpoint, region, and how credentials should be resolved. I use a short alias like db. Then check that list / write / read / delete all work:

tagbackup bucket verify --bucket=db

Config stays local (~/.config/tagbackup/config.yaml on Linux/macOS, mode 0600). Nothing dials home to tagbackup.com. On a server I prefer IAM roles or environment variables over static keys in the YAML — the docs cover the resolution order.

The nightly routine

Cron runs two steps. First, dump and push. Pipe pg_dump straight into TagBackup — - means stdin, and --filename gives the object a sensible name:

pg_dump mydb | tagbackup push - \
  --bucket=db \
  --tag=nightly,prod \
  --filename=dump.sql \
  --non-interactive

--non-interactive matters in scripts: if something would normally prompt, TagBackup exits with a usage error instead of hanging.

Second, prune anything older than thirty days that matches both tags (+ means AND):

tagbackup delete \
  --bucket=db \
  --tag=nightly+prod \
  --older-than=30d \
  --force \
  --non-interactive

I usually dry-run once by hand (--dry-run instead of --force) before trusting cron with deletes. After that, the crontab line is boring on purpose — dump, push, prune, sleep.

A minimal sketch:

15 2 * * * /usr/local/bin/backup-mydb.sh >>/var/log/backup-mydb.log 2>&1

Put the two commands above in that shell script. Use absolute paths to pg_dump and tagbackup if cron’s PATH is thin.

When I need a restore (laptop)

On the laptop I configure the same bucket alias (read-only credentials are fine). Then:

tagbackup pull --bucket=db --tag=nightly+prod --latest

That downloads the newest object matching both tags into the current directory as dump.sql. To restore into a local database:

tagbackup pull --bucket=db --tag=nightly+prod --latest --output=- | psql mydb

Progress goes to stderr when --output=-, so the SQL stream stays clean on stdout. If nothing matches, the exit code is 5 — useful in scripts.

I can also list what is there before pulling:

tagbackup files --bucket=db --tag=nightly+prod

Optional: the local web UI

When I want to see the dumps rather than filter them from a shell, I run:

tagbackup serve

It opens a browser UI bound to 127.0.0.1 only — never exposed on the network. Browse buckets, filter by tag, download a specific row. Bucket setup still happens via the CLI. For “give me the latest nightly prod dump,” I still prefer pull --latest.

Where to go next

This piece is the human version of the classic workflow. If you want the fuller how-tos:

And the reference docs: tagbackup.com/docs. Install from GitHub Releases if you prefer not to use the install script.

That is the whole job: dump on the server, tag it, keep a month, pull when needed. Credentials stay where they belong.