MSaving

Backups and restore

Nightly backups, keeping a copy elsewhere, and restoring one.

What’s backed up

Every night (and before every update) the server writes a backup of the whole database into the backups volume and keeps about 6 days of them. They’re on the same disk as the server, so copy them somewhere else regularly:

docker compose cp msaving:/backups ./msaving-backups

The folder also holds purged_accounts.jsonl, the list of accounts deleted for good; a restore needs it too. Backups contain password hashes: keep the copies private.

Restore

This replaces everything on the server with the backup. It works the same on the same machine or on a new one (a fresh install first, then these steps). Run it in the msaving folder, with your copy in ./msaving-backups:

docker compose stop msaving
docker compose cp ./msaving-backups/msaving-<time>.dump db:/tmp/restore.dump
docker compose exec db dropdb -U postgres --force --maintenance-db=template1 postgres
docker compose exec db createdb -U postgres postgres
docker compose exec db psql -U postgres -c 'alter default privileges revoke execute on functions from public'
docker compose exec db sh -c "pg_restore --list /tmp/restore.dump | grep -v -E ' (SCHEMA|COMMENT) - (SCHEMA )?public ' > /tmp/restore.list && pg_restore -U postgres --no-owner --single-transaction --exit-on-error --use-list=/tmp/restore.list -d postgres /tmp/restore.dump"
docker compose start msaving
docker compose exec -T msaving sh -c 'cat >> /backups/purged_accounts.jsonl' < ./msaving-backups/purged_accounts.jsonl
docker compose exec msaving python -m app.admin repurge

The last two lines delete again any account that was deleted for good after that backup was taken (the backup still has it). On a new machine the server’s list of those is empty, so the first of them adds yours. Accounts closed or restored after the backup show in the server’s log as account_event lines; redo them by hand if it matters.

With your own database

There’s no db container, so the server’s image runs the restore against ADMIN_DATABASE_URL. It empties the public and msaving schemas instead of dropping the database (a hosted database may not allow that):

docker compose stop msaving
docker compose run --rm --no-deps -T msaving sh -c '
  cat > /tmp/restore.dump &&
  psql "$ADMIN_DATABASE_URL" -q -v ON_ERROR_STOP=1 -c "drop schema if exists msaving cascade" \
    -c "drop schema public cascade" -c "create schema public" \
    -c "alter default privileges revoke execute on functions from public" &&
  pg_restore --list /tmp/restore.dump | grep -v -E " (SCHEMA|COMMENT) - (SCHEMA )?public " > /tmp/restore.list &&
  pg_restore --no-owner --single-transaction --exit-on-error --use-list=/tmp/restore.list \
    -d "$ADMIN_DATABASE_URL" /tmp/restore.dump' < ./msaving-backups/msaving-<time>.dump
docker compose start msaving

Then the same two purged_accounts.jsonl and repurge lines as above. This empties those two schemas completely: use a database only MSaving uses.

Tested by hand on a new install (the built-in database, and a separate Postgres 17): sign-in with two-step verification and recovery codes, shared books, transactions and an account deleted after the backup. How backups work: Background jobs.

Edit this page · Report a problem