server/seed_demo.py seeds a realistic DEMO project (complete SOP + a spread of Work Packages: issued, gated, multi-discipline master with split instances, overdue, over-threshold draft) via the API. --clean removes it. DEPLOYMENT.md documents both smoketest.py and seed_demo.py, including the localStorage caveat (seeded project shows in the UI picker; seeded SOP/WPs are SQL-only until Phase 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
10 KiB
Deployment
Audience: the IT admin standing this up inside the firewall. This covers the SQL-backed deployment — NGINX serving the static front end and a Python API backed by PostgreSQL.
The repo already contains everything needed to run it as a Docker stack:
Dockerfile, docker-compose.yml, the nginx/ config, the front end in
html/, and the API in server/. The detailed container reference (endpoints,
password rotation, day-to-day commands) lives in
server/README.md — this doc is the start-to-finish guide.
[ your TLS reverse proxy / traefik ] ← HTTPS terminates here
│ (external "proxy" network)
┌────▼────┐ internal network ┌──────────┐ ┌────────────┐
browser ───────────────────────│ nginx │ ───── /api/ ───────> │ api │ → │ postgres │
│ (html/) │ │ FastAPI │ │ (db) │
└─────────┘ └──────────┘ └────────────┘
Everything runs inside your firewall; the app makes no outbound internet calls (logo and scripts are local).
Architecture note: all static files live under
html/and are baked into the nginx image at build time (not bind-mounted). So after any front-end change you rebuild thewebserverimage (see Updating below). The API image is built from the rootDockerfile.
1. Prerequisites
- A Linux host with Docker and Docker Compose v2 (
docker compose …). - An external Docker network named
proxythat your TLS-terminating reverse proxy also sits on (the compose file marks itexternal: true):If you don't run a separate reverse proxy, you can instead publish the nginx container's port 80 directly (see the note in step 4) and terminate TLS there.docker network create proxy - The repository checked out on the host.
2. Create the database credentials (.env)
Create a file named .env in the project root (same folder as
docker-compose.yml). It is git-ignored and must never be committed.
# .env — project root
POSTGRES_DB=wpsuite
POSTGRES_USER=wpsuite
POSTGRES_PASSWORD=<strong-random-password>
# Must match the POSTGRES_* values above. Host is the compose service name "db".
DATABASE_URL=postgresql+psycopg://wpsuite:<strong-random-password>@db:5432/wpsuite
Generate a strong password with openssl rand -base64 32.
These are the only credentials in the system: the db container initialises
Postgres from POSTGRES_*, and the api container connects with the matching
DATABASE_URL. Neither value appears in the compose file or in git.
3. Point your reverse proxy at the nginx container
The nginx container listens on port 80 on the proxy network and expects
TLS to be terminated upstream (by your reverse proxy / traefik). Route your
chosen hostname (e.g. wp-suite.company.local) to the nginx_webserver
container on that network. The container already proxies /api/ to the api
service internally — no extra app config needed.
4. Bring it up
From the project root:
docker compose up -d --build # builds the api + nginx images, starts all three containers
docker compose ps # confirm nginx_webserver, wp_api, wp_db are running/healthy
docker compose logs -f api # watch the API start (Ctrl-C to stop following)
The database schema is created automatically on first API start — no manual
CREATE TABLE. The Postgres data lives in the named volume pgdata and
survives docker compose down (only down -v deletes it).
No separate reverse proxy? Publish nginx directly by adding a
ports:mapping to thewebserverservice (e.g."8080:80") and terminate TLS at whatever sits in front of it. The internalapi/dbcontainers should never be published.
5. Verify
# API liveness (from the host, through the proxy hostname)
curl https://wp-suite.company.local/api/health # → {"ok": true}
# Interactive API docs
# https://wp-suite.company.local/api/docs
Then load the site in a browser: the home page should prompt to select or create a project. Create one, complete an SOP, and confirm a row appears:
docker compose exec db psql -U wpsuite -d wpsuite -c "select id, name from projects;"
Automated smoke test
server/smoketest.py exercises the whole stack end-to-end (health → project →
SOP → Work Package → the AWP issue gate → status → metrics → comments → cascade
cleanup). Stdlib only — no pip/jq.
# Through the proxy (use --insecure for a self-signed internal cert):
python3 server/smoketest.py https://wp-suite.company.local --insecure
# Or from inside the api container (hits FastAPI directly):
docker compose exec api python /app/server/smoketest.py http://localhost:8000
# Add --keep to leave a demo project in the DB so you can open it in the UI.
Exit code 0 and "ALL PASS" means the API, the Python logic, and SQL are all working. It cleans up after itself (the test project and its SOP/WPs are deleted via cascade); a single tagged test comment remains (there's no comment delete endpoint).
Loadable demo project
server/seed_demo.py populates a realistic DEMO project (a complete SOP plus
a spread of Work Packages: issued, gated, a multi-discipline master with split
instances, an overdue one, an over-threshold draft) so there's data to look at.
python3 server/seed_demo.py https://wp-suite.company.local --insecure
python3 server/seed_demo.py https://wp-suite.company.local --clean # remove it later
What shows where: the DEMO project is API/SQL-backed, so it appears in the home-page project picker right away (this is the visible proof that the projects → SQL path works end-to-end). The DEMO SOP and Work Packages are written to SQL too, but the current front end still reads SOPs/WPs from the browser, so they won't render in the Creator/Dashboard until the Phase 2 wiring. Inspect them at the SQL layer with
smoketest.pyor:docker compose exec db psql -U wpsuite -d wpsuite \ -c "select number, subject, status from work_packages order by number;"
What is stored in SQL today
Be aware of the current persistence split — the API + Postgres are fully deployed, and:
| Data | Stored in PostgreSQL today? |
|---|---|
| Projects | Yes — the front end is API-first (/api/projects), falling back to the browser only if the API is unreachable. |
| Comments / feedback | Yes — every feedback surface posts to /api/feedback. |
| SOPs | Endpoints exist (/api/sops); the front end still keeps the SOP in the browser (namespaced per project). Wiring it to the API is the remaining Phase 2 step. |
| Work Packages | Same — /api/wps (+ issue/status/metrics) exist and are ready; the creator still saves to the browser per project. |
So a fresh deployment gives you shared, server-stored projects and comments immediately. Moving SOPs and Work Packages off the browser and onto the API (so they're shared across users too) is a front-end change only — the database and endpoints are already in place.
Data model (PostgreSQL)
| Table | Holds | Key columns |
|---|---|---|
projects |
top-level construction projects | name, number, client, division, site, sample, data |
sops |
project SOP baselines | project_id → projects, name, number, complete, data (full SOP JSON) |
work_packages |
individual IWPs | project_id → projects, sop_id → sops, parent_id (split instances), number, subject, type, status, issued_at, data (full WP JSON) |
comments |
feedback from any page | source, sop_id, wp_id, step, author, text, extra |
The complete client document is stored verbatim in each row's data JSON
column; frequently-listed fields are promoted to real columns for filtering.
Endpoints (summary)
Projects GET/POST /api/projects, GET/DELETE /api/projects/{id} ·
SOPs GET/POST /api/sops, GET /api/sops/latest, GET/DELETE /api/sops/{id} ·
Work Packages GET/POST /api/wps, GET/DELETE /api/wps/{id},
POST /api/wps/{id}/issue, POST /api/wps/{id}/status, GET /api/wps/metrics ·
Comments POST /api/comments (and /api/feedback), GET /api/comments.
List/latest/metrics accept a project_id (and sop_id) filter. Full reference
and request shapes: /api/docs and server/README.md.
Updating after a change
git pull
docker compose up -d --build webserver # front-end change (html/) — rebuild the baked image
docker compose up -d --build api # backend change (server/)
Backups & retention
The whole dataset is in the pgdata volume — back it up on a schedule:
# Backup (run from project root)
docker compose exec -T db pg_dump -U wpsuite wpsuite > backup-$(date +%F).sql
# Restore
docker compose exec -T db psql -U wpsuite -d wpsuite < backup-YYYY-MM-DD.sql
Schema migrations (important)
Tables are auto-created on API startup (Base.metadata.create_all). This
creates missing tables, but it does not alter existing ones. The
multi-project work added the projects table and new columns
(sops.project_id, work_packages.project_id / parent_id / issued_at):
- On a fresh database these appear automatically — nothing to do.
- On a database that already has data from an older schema, add the new
columns with a migration (introduce Alembic) or apply them manually with
ALTER TABLEbefore deploying — don't rely oncreate_allfor column changes.
Local trial without Postgres
For a quick local look, the API falls back to a SQLite file when DATABASE_URL
is unset (sqlite:///./wpsuite.db) — see server/README.md
§ Local dev. The front end alone can also be served statically from html/
(it falls back to browser storage when the API isn't reachable).