Install & run¶
nodum is two artifacts from one codebase:
- the Docker image is the full app — API + the built React web UI;
- the PyPI wheel is the CLI / library — the
nodumcommand and the HTTP API, with no web UI.
Both need a PostgreSQL to point at (nodum does not bundle one). Pick the track that matches how you want to run it.
Docker — the full app¶
The image is a multi-stage build: a Node stage compiles the React SPA, and the Python stage installs
the package and copies the bundle to NODUM_WEB_DIST. The entrypoint waits for Postgres, runs
init-db, sets the main password from the admin secret (only if unconfigured), then serves on
0.0.0.0:8600. A deploy is therefore: declare the image, point NODUM_DATABASE_URL at a Postgres,
provide a password secret — done.
With the example compose file¶
docker-compose.example.yml is a turnkey start — the nodum image, a Postgres, and a password secret:
curl -O https://raw.githubusercontent.com/vcoeur/nodum/main/docker-compose.example.yml
echo 'change-me' > nodum_admin_password.txt # the initial main password
docker compose -f docker-compose.example.yml up # → http://127.0.0.1:8600
The example pulls ghcr.io/vcoeur/nodum:latest. To build from a checkout instead, comment that
image: line and uncomment build: . in the compose file.
In your own deployment¶
Point the image at your Postgres and supply the password as a secret (preferred) or an env var:
services:
nodum:
image: ghcr.io/vcoeur/nodum:latest
environment:
NODUM_DATABASE_URL: postgresql://user:pass@your-db:5432/nodum
NODUM_ADMIN_PASSWORD_FILE: /run/secrets/nodum_admin_password
NODUM_COOKIE_SECURE: "1" # set when TLS is terminated in front of nodum
secrets:
- nodum_admin_password
ports:
- "8600:8600"
The image does not bundle Postgres — bring your own managed or containerised database. The admin
secret only sets the password on first boot; a later nodum auth set-password is not clobbered on
restart.
PyPI — the CLI / library¶
This gives you the nodum command and the HTTP API. It ships no web UI — the React bundle is
image-only, so nodum serve without NODUM_WEB_DIST serves the API alone. Use this track for
scripting, automation, or embedding the service.
Point it at a Postgres and initialise:
export NODUM_DATABASE_URL=postgresql://nodum:nodum@localhost:5436/nodum
nodum init-db # create the schema + seed the kind lookup tables (idempotent)
nodum auth set-password # set the main password that gates the API + web
Need a local Postgres for development? The repo's docker-compose.yml publishes one on host port
5436 (docker compose up -d), which is what the default NODUM_DATABASE_URL above expects.
Configuration¶
All configuration is environment variables. A local .env at the working directory is read if
present — copy .env.example to start. The only required value is NODUM_DATABASE_URL.
| Variable | Default | What it does |
|---|---|---|
NODUM_DATABASE_URL |
postgresql://nodum:nodum@localhost:5436/nodum |
PostgreSQL connection string. Required. |
NODUM_API_HOST |
127.0.0.1 (image: 0.0.0.0) |
API bind address. |
NODUM_API_PORT |
8600 |
API port. |
NODUM_WEB_DIST |
unset (image: the built bundle) | Path to the built SPA. When set, serve mounts the web UI; when unset, the API runs alone. |
NODUM_COOKIE_SECURE |
0 |
Set to 1 to mark the session cookie Secure (behind a TLS-terminating proxy). |
NODUM_ADMIN_PASSWORD_FILE |
unset | File whose contents seed the main password on first boot (auth ensure-password). |
NODUM_ADMIN_PASSWORD |
unset | Inline alternative to NODUM_ADMIN_PASSWORD_FILE. Prefer the file/secret form. |
Authentication¶
The network surfaces — the HTTP API and the web UI — are gated by a single main password, set
from the CLI on the machine where nodum runs. The local CLI is trusted: it sets the secret and
never logs in. Until a password is set the install is locked — protected routes return 503
pointing at the CLI. Multi-user accounts are out of scope; this is one shared password.
nodum auth set-password # set/replace it (prompts twice, or reads a piped stdin line)
nodum auth status # is a password configured? (never prints the hash)
How it works:
- Storage. A single-row
auth_secrettable holds an argon2 hash of the password plus a random signing key. argon2 runs only at login and atset-password— never on the per-request path. - Tokens (dual auth). Login mints a session token signed with the signing key (
itsdangerous, 7-day expiry). Browsers carry it in an HttpOnly, SameSite=Strict cookie (SecurewhenNODUM_COOKIE_SECURE=1); API/CLI clients send it asAuthorization: Bearer <token>. The per-request check verifies only the cheap signature — cookie first, then the Bearer header. - Rotation.
set-passwordrecomputes the hash but preserves the signing key, so changing the password does not invalidate live sessions. - Defence in depth. Every response carries
Content-Security-Policy: default-src 'self',X-Content-Type-Options: nosniff, andX-Frame-Options: DENY. - Open routes.
GET /healthz,POST /auth/login,POST /auth/logout,GET /auth/session, and (when the SPA is mounted)GET /+/assets. Everything else requires a valid session. - Bootstrap.
auth ensure-passwordsets the password fromNODUM_ADMIN_PASSWORD_FILE/NODUM_ADMIN_PASSWORDonly when unconfigured — this is what the Docker entrypoint uses for a hands-off first boot.
Migrating an older database¶
If you have a pre-typed (MVP) database from before the metamodel, upgrade it in place — idempotently:
It adds the kind columns, seeds the lookup tables, drops the old type-as-node rows (their edges
cascade), backfills kinds (content nodes become Note, carrying their old data.type into role),
then enforces the new constraints. Safe to re-run.
Development from a source checkout¶
git clone https://github.com/vcoeur/nodum.git
cd nodum
make db-up # start the local Postgres (docker-compose, host port 5436)
make dev-install # uv sync --all-groups
make init-db # create the schema + seed kind tables
uv run nodum auth set-password
make test # pytest (needs the database up)
For the web UI (React + Vite, in frontend/):
make frontend-install # npm ci
make frontend-dev # Vite dev server on http://127.0.0.1:5700 (proxies the API to 8600)
# …or build it and serve through FastAPI on 8600:
make dev-web
Dev ports: the HTTP API serves on 127.0.0.1:8600, the Vite dev server on 5700 (preview 5701),
and the local Postgres is published on host port 5436. Run make help for the full target list.
The package version is derived from the git tag (vX.Y.Z) at build time and is never committed.