Skip to content

Operations

Everything lives in one volume: app.db and photos/. Settings → Backup downloads all of it as one .tar.gz, laid out exactly like the data directory.

Set BACKUP_TOKEN (openssl rand -hex 32) and a cron job can pull the same archive:

Terminal window
curl -sf -H "Authorization: Bearer $BACKUP_TOKEN" \
https://example.com/api/backup -o "mash-$(date +%F).tar.gz"

The token is valid for this endpoint only — it cannot log in to the admin. A wrong or stale token answers 401, so curl -f in a cron job fails loudly instead of silently archiving a login page.

A backup restores by unpacking it over the data volume while Mash is stopped:

Terminal window
docker compose stop mash
tar xzf mash-backup-….tar.gz -C /path/to/volume
docker compose start mash

manifest.json inside the archive records when the backup was taken and what it contains; restore ignores it.

You can also back up the volume directly instead of going through the app:

Terminal window
# With the container stopped (or quiesced), copy the data directory:
docker compose stop
cp -a ./data ./mash-backup-$(date +%F)
docker compose start

For hot backups, prefer SQLite’s own backup over copying a live app.db (the container image doesn’t ship the sqlite3 CLI, so run it from the host against the mounted volume):

Terminal window
sqlite3 ./data/app.db ".backup ./mash-backup-$(date +%F).db"

then copy photos/ alongside it.

Terminal window
docker compose pull && docker compose up -d

Migrations run automatically at boot. Before applying any, Mash snapshots the database to app.pre-<n>.db next to app.db (one snapshot is kept — the state just before the most recent upgrade), so a bad upgrade can be rolled back by stopping the container, renaming the snapshot over app.db, and starting the previous image. Still take a full backup first if you’re jumping many versions — the snapshot covers the database, not photos/.

To see what you’re running, the foot of Settings shows the version and the commit the build was made from, linked to the commit on GitHub.

Mash sits happily behind Caddy, nginx, or Traefik terminating TLS. The one rule: PUBLIC_BASE_URL must be the public URL (e.g. https://sauce.example.com), not the internal address. It’s baked into your QR codes and used for CSRF origin checks, so a mismatch breaks form submissions.

ADMIN_PASSWORD is reapplied from the environment on every start: change it in .env, docker compose up -d, done. The same goes for ADMIN_EMAIL. Changing SESSION_SECRET signs everyone out; changing VOTE_SALT resets vote dedup identity (existing counts stay).

If you’ve configured email (SMTP_URL + MAIL_FROM), the sign-in page shows a Forgot your password? link to /reset. Enter the admin email and Mash sends a reset link, good for 30 minutes, to set a new one. The page confirms “if that address is the admin’s, a link is on its way” whether or not it matched — a stranger can’t use it to discover the admin address.

With no email configured the link doesn’t appear; recover instead by setting ADMIN_PASSWORD in the environment and restarting, as above.

One catch, and it follows from that same rule: because ADMIN_PASSWORD is reapplied from the environment on every start, a password set through the reset flow lasts only until the next restart reapplies the env value. To make it permanent, update ADMIN_PASSWORD in .env to match.