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
secretsmakes the database password;dbstarts with it.app/launch.pymakes one password per login (api_login,purge_login,fx_login), kept in thesecretsvolume, waits for the database, then runs backend/app/jobs/migrate.py: every migration not applied yet (recorded inmsaving.migrations), the logins, and, while there’s no admin, a new setup code printed in the log.- It starts uvicorn with only
api_loginin its environment, and the scheduler with the jobs’ logins and the owner’s (for the backup). - 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
VERSIONfile 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=offstops it). - One-click (opt-in): with
COMPOSE_PROFILES=updater, theupdaterhelper runs. It mounts the Docker socket and the checkout (writable) and touches/updates/updater_aliveevery 5 seconds. Update now makes the API write/updates/requested; the helper runsgit pull --ff-onlyas the checkout’s owner (so the files stay the operator’s), thendocker compose up -d --build --no-depsfor the services already running except itself, and writesrunning,doneorfailedwith 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=0stays 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 pulland the helper refuse, and the helper says so. - Within
/v1the 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.