Skip to content

Back up and restore

Two things to back up, both in the data volume (/data in the container):

  • the SQLite database, SUFS_DB_PATH (/data/sufs-tracker.db): users, accounts, memberships, sessions, invitations, API tokens, and every account's decisions, log, students and the records of attached files;
  • the attached files themselves, SUFS_FILES_DIR (/data/files): one directory per account, one file per attachment, named by a content hash. These files are written once and never modified, so copying only the new ones is enough.

Don't copy the file while the server is running

The database runs in WAL mode, so a plain cp of the .db file can miss recent writes or produce an inconsistent copy. Use the backup command, which uses SQLite's online backup and is safe at any time.

One backup

sufs-tracker backup /path/to/sufs-2026-10-03.db

With Docker, stream it out of the container so you don't need a shared volume:

docker compose exec -T sufs-tracker sufs-tracker backup - > sufs-$(date +%F).db

The result is a normal SQLite file. You can open it with any SQLite tool to inspect it.

The attached files are plain files; copy the directory, or stream it out as a tar archive:

docker compose exec -T sufs-tracker tar -C /data -cf - files > sufs-files-$(date +%F).tar

Because attachments never change once written, rsync -a from the volume's files/ into your backup location (or any tool that copies new files only) keeps a complete copy cheaply.

A nightly routine

On the host, as the user who runs docker compose, with the compose file in /srv/sufs-tracker:

# nightly at 03:17, keep 30 days
17 3 * * * cd /srv/sufs-tracker && docker compose exec -T sufs-tracker sufs-tracker backup - > backups/sufs-$(date +\%F).db && find backups -name 'sufs-*.db' -mtime +30 -delete
# the attached files: copy whatever is new into one mirror directory
27 3 * * * cd /srv/sufs-tracker && docker compose exec -T sufs-tracker tar -C /data -cf - files | tar -C backups -xf -

(% must be escaped in crontab.) Put backups/ on storage that is itself backed up off the machine, or add a line that syncs it there. The database is small: a household's tracker is well under a megabyte. The files directory grows with the receipts, typically a few hundred kilobytes each.

Without Docker, the same with uv run sufs-tracker backup … from the checkout, with SUFS_DB_PATH set as for the server.

Checking a backup

sqlite3 sufs-2026-10-03.db 'PRAGMA integrity_check; SELECT COUNT(*) FROM users; SELECT COUNT(*) FROM log;'

Restoring

  1. Stop the server: docker compose stop sufs-tracker.
  2. Replace the database file in the volume with the backup, and remove any -wal and -shm files next to it:

    docker compose run --rm -T --entrypoint sh sufs-tracker -c 'cat > /data/sufs-tracker.db && rm -f /data/sufs-tracker.db-wal /data/sufs-tracker.db-shm' < sufs-2026-10-03.db
    
  3. If attachments are to be restored too, put the files directory back under /data the same way (for example docker compose cp backups/files sufs-tracker:/data/). Files in the database with no matching file on disk show as not on this device in the page until they are; files on disk with no record are removed at startup.

  4. Start it again: docker compose start sufs-tracker. If the backup is from an older version, the schema is upgraded on startup.

Everyone is still signed in afterwards (sessions are in the database too), unless the backup predates their session.

Users' own exports

Users can export their account as .json (records) or .zip (records plus every attached file) from the page at any time; it's a per-account backup they control, and it imports into a standalone tracker. It doesn't replace the database backup: it has no logins, memberships, tokens or other accounts.