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.lockpins 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.shhands Docker agit archiveof the commit, so a database, a measured series or a.envof a working checkout cannot reach it..dockerignorerefuses 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
Hostis refused (400): a page that points its own domain at the server reaches nothing. Withlocalhost, 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 inPTX_TRUSTED_PROXIESonly;*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
localhostis 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_PRINCIPALis refused at start and never honoured;--reloadand--sharecannot 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:
- 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.envfile is read: none is in the image, and one placed where the app would find it is refused. /datamust 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)".- 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. - 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/healthevery 30 seconds (docker/healthcheck.py);docker compose psshows “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.mdhas 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.
Related topics
From the app’s documentation, docs/05-development/run-with-docker.md · Hyprisma 1.0.5