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.