All method pages

MethodDevelopment

partial some parts checked against the code, others not yet · what the statuses mean

Run with Docker

In simple terms

The app ships as one Docker image: the page, the server and its solvers, run as a non-root user. Everything it keeps — the database, the activity log, stored results, imported series, each workspace’s own default books, the backups it makes — lives in one folder, /data, which you mount: a Docker volume on a server, a folder of yours on your own computer. Two ways to run it:

  • On a server, behind Caddy, which gets and renews the HTTPS certificate from Let’s Encrypt. People open https://<your domain>.
  • On your own computer, with docker run. Only that computer can open it (http://localhost:8000), and your data stays in your folder.

The commands, step by step, are in docker/README.md in the repository. This page explains what the image holds and how the server behaves in it.

What the image holds

  • The Python packages exactly as requirements.lock pins them, each file checked against its hash, from prebuilt wheels only — the lock has them for linux/amd64 and linux/arm64 alike, HiGHS (highspy) included.
  • The server code (backend/, without its tests), the page built inside the image (frontend/dist), and the data the app ships: the default assumption books, the example projects and the two resource workbooks the shipped sites read.
  • Nothing else. The image is built from committed files only: scripts/build_image.sh hands Docker a git archive of the commit, so a database, a measured series or a .env of a working checkout cannot reach it. .dockerignore refuses them a second time, for a build started any other way; a test reads it with Docker’s own matching rules.

Its labels carry the version (org.opencontainers.image.version, the release number of the code inside — the build checks the two agree), the commit and its date.

How the server runs in the container

The container starts backend/scripts/serve.py --behind-proxy. That mode serves the built page and the API under /api on one port, 8000, in plain HTTP — HTTPS is the proxy’s job — and answers to one name, the one in PTX_PUBLIC_HOST:

On a server, behind Caddy On your own computer
PTX_PUBLIC_HOST your domain, e.g. app.example.com localhost (the image’s default)
PTX_TRUSTED_PROXIES Caddy’s address on the compose network not set
What browsers speak HTTPS to Caddy, which forwards plain HTTP plain HTTP to localhost
The session cookie __Host-ptx_session, Secure ptx_session
The first account made on the server’s command line set up on the page, as on a Mac

The rules, checked before the server listens — each refusal is one sentence in the container’s log:

  • One name. A request for any other Host is refused (400): a page that points its own domain at the server reaches nothing. With localhost, this computer’s own names are accepted (localhost, 127.0.0.1, [::1]).
  • Only the proxy is believed. The scheme (X-Forwarded-Proto) and the client’s address (X-Forwarded-For) are read from the addresses in PTX_TRUSTED_PROXIES only; * and “every address” are refused. The client’s address is what the sign-in throttle counts, so one person’s failed sign-ins slow that person down, not everyone behind the proxy.
  • Behind a proxy, everything comes through it. A request that did not arrive through the proxy over HTTPS is refused (403) — except the health route, which Docker asks from inside the container.
  • No plain HTTP to a public name. Without a proxy, only localhost is served: anything else would carry passwords in clear.
  • One origin. A change must say which page sent it (Origin), and that page must be the app’s own address — not another site, another port or a development page. There is no CORS at all.
  • No shortcut. The development switch PTX_DEV_PRINCIPAL is refused at start and never honoured; --reload and --share cannot be combined with this mode.
  • Behind a proxy, the first account is made on the machine. Whoever reached a fresh server’s page first would otherwise become its admin. The page refuses it and names the command: docker compose exec app python backend/scripts/set_up_account.py --email … --name "…".

Without --behind-proxy nothing changes: the server listens on 127.0.0.1 only, as before.

What the container does at start

docker/entrypoint.sh, in order; each refusal stops the container with a sentence:

  1. Every data path is under /data. The image sets them all (table below); one that points elsewhere is refused. The default books and the resource workbooks the app ships are read from the image itself, read-only, so an update brings the new ones. No .env file is read: none is in the image, and one placed where the app would find it is refused.
  2. /data must be writable by the container’s user (uid 10001). For a folder of yours on Linux, run the container as yourself: --user "$(id -u):$(id -g)".
  3. The folders are made — readable by that user only — and the database is created or migrated by the app’s own migration runner (backend/scripts/migrate_db.py), in WAL mode as the app expects. A database a newer version has already migrated is refused: run that version, or restore the backup made before the update.
  4. The server starts. A command given to the container runs instead, after the same steps — e.g. docker compose run --rm app python backend/scripts/set_up_account.py ….
Variable In the image
PTX_DATA_ROOT /data
PTX_DB_PATH · PTX_AUDIT_DB_PATH /data/ptx_projects.db · /data/audit.db
PTX_DATA_RESULTS · PTX_DATA_PROCESSED · PTX_DATA_RAW /data/results · /data/processed · /data/raw
PTX_WORKSPACE_DAB_DIR · PTX_PRIVATE_DIR · PTX_BACKUP_DIR /data/dab_workspaces · /data/private · /data/backups
PTX_EXPORTS_TMP /tmp/ptx-exports (each download’s folder is deleted once it is sent)
PTX_DAB_DIR · PTX_SERIES_ROOT /app/backend/data/dab · /app (shipped with the image)
PTX_PUBLIC_HOST localhost (the server recipe sets the domain)

On a server, PTX_GRID_WORKERS (the worker processes of Try every combination) is set to 2 by the compose file: each worker holds a solver in memory, and inside a container the processor count Python sees is the host’s, not the container’s share.

Health, back up, update

  • Health. Docker asks GET /api/health every 30 seconds (docker/healthcheck.py); docker compose ps shows “healthy”.
  • Back up. backend/scripts/backup_db.py, run inside the container while the app runs, copies the database and the activity log with the SQLite backup API — checked, one file each — with each workspace’s own default books beside them, into /data/backups. Stored solves, saved runs and imported series are files under /data: a full backup copies the whole volume (docker/README.md has the commands). Keep backups on disks you control: they hold every account’s password hash.
  • Update. Pull the new image and start it again; the database is migrated at start. Back up first: an older image cannot open a database a newer one migrated.

What the image does not do

  • The Excel package export needs files that are not in the image (the cockpit workbook and the Windows engine folder); the export says what is missing.
  • Sharing on the office Wi-Fi (serve.py --share) belongs to a Mac. In the container, Workspace settings → Sharing on this network still describes that share, not the server’s address. To invite a colleague, send the link made “for someone on this Mac”: on a server it carries the server’s address.
  • Measured series never leave the machine they were ingested on. A database copied from a Mac keeps its projects, but a dataset whose series is not shipped with the app cannot be read in the container.

Building the image

scripts/build_image.sh builds from the committed code (git archive of HEAD) and prints the digest; --platform linux/amd64,linux/arm64 --push builds both platforms with Docker Buildx. The repository’s workflow docker/image.workflow.yml (copy it to .github/workflows/image.yml to enable it) runs on a release tag (v…) or when started by hand: it builds, starts the amd64 image on an empty volume, checks the health route, the version, the page, a first account, an example project and a solve — then pushes to ghcr.io/hyprisma/app, only for a tag or when “push” is ticked.

From the app’s documentation, docs/05-development/run-with-docker.md · Hyprisma 1.0.5