MSaving

Running an instance

What an operator runs: the compose file, its containers and volumes, first start, HTTPS, updates, backups.

Each instance is a checkout of the monorepo on the operator’s machine, run with its docker-compose.yml. There is no hosted version, no shared environment and no published image: Docker builds the server from the checkout, and developers run the same stack on their laptops. The operator’s steps are in Getting started; this page is how it works.

The stack

Service Image What it does
secrets msaving:local, built from the Dockerfile First start only: writes the built-in database’s password into the secrets volume, then exits
db postgres:17 The built-in database. Reads its password from the file. Not published on any port
msaving msaving:local app/launch.py: the API, the web app’s files and the daily jobs. Published on BIND:PORT (127.0.0.1:8080)
caddy caddy:2 Profile https: HTTPS for DOMAIN, certificates automatic
updater docker:29-cli with git, built inline Profile updater: one-click updates from the web app (see below)

Volumes: secrets (database passwords), db, backups (nightly dumps and PURGE_LOG), updates (the updater’s request and status files), caddy. Every setting is optional, with a safe default; the full list is in the README and .env.example, and in Environment variables.

First start

  1. secrets makes the database password; db starts with it.
  2. app/launch.py makes one password per login (api_login, purge_login, fx_login), kept in the secrets volume, waits for the database, then runs backend/app/jobs/migrate.py: every migration not applied yet (recorded in msaving.migrations), the logins, and, while there’s no admin, a new setup code printed in the log.
  3. It starts uvicorn with only api_login in its environment, and the scheduler with the jobs’ logins and the owner’s (for the backup).
  4. The operator opens the address, enters the setup code, and creates the admin. See Sign-in and accounts.

Own database

ADMIN_DATABASE_URL (the owner of any Postgres 15+, or a Supabase project’s direct connection) replaces the built-in one; start with --scale db=0. The server creates its logins there with the same host, port, database and options. On Supabase the baseline takes away everything Supabase grants its browser roles.

HTTPS and reaching it

The server listens on 127.0.0.1 until the operator chooses: Caddy with their domain (profile https), Tailscale (tailscale serve --bg 8080, a private HTTPS address, no open ports), Cloudflare Tunnel, or BIND=0.0.0.0 on a trusted home network over plain HTTP. The mobile app needs HTTPS. uvicorn trusts forwarded headers from any proxy, so the client address and X-Forwarded-Host work behind each of these.

Updates

  • git pull && docker compose up -d --build. On start, the launcher sees migrations not applied yet, takes a backup, then applies them.
  • The admin’s page shows the running version (the VERSION file the image was built from) and the newest release from the project’s GitHub releases (backend/app/core/updates.py, checked at most every 6 hours, only when the admin opens the page; UPDATE_CHECK=off stops it).
  • One-click (opt-in): with COMPOSE_PROFILES=updater, the updater helper runs. It mounts the Docker socket and the checkout (writable) and touches /updates/updater_alive every 5 seconds. Update now makes the API write /updates/requested; the helper runs git pull --ff-only as the checkout’s owner (so the files stay the operator’s), then docker compose up -d --build --no-deps for the services already running except itself, and writes running, done or failed with the time to /updates/status, which the page polls. It skips itself because compose inside the helper would mount the helper’s /project, not the folder on the host; it skips stopped services so a database left off with --scale db=0 stays off. Only the helper can reach Docker and the checkout; the API can only leave the request file. Off by default because the Docker socket is root-level access to the machine.
  • A server whose checkout is on a release tag (pinned) stays there: git pull and the helper refuse, and the helper says so.
  • Within /v1 the API only adds, so phones keep working with older and newer servers.

Backups

Nightly and before every update, into backups, about 6 days kept, owner-only files. Restoring: the README’s docker commands (tested by hand) and the $ lines in backend/app/jobs/backup.py (run by the tests), then python -m app.admin repurge. See Background jobs.

Developers

The same compose file (docker compose up -d --build from a checkout), or backend/scripts/dev.sh for the API with reload against the compose database (published on 127.0.0.1:5433 by docker-compose.dev.yml) and npm run dev in web/, which proxies /v1. Tests use a throwaway Postgres. See Run it locally.

Edit this page · Report a problem