Rewrite DEPLOYMENT.md for the SQL-backed Docker deployment
Replaces the stale non-Docker/systemd guide with an admin-facing, start-to- finish guide for the actual stack (nginx serving html/, FastAPI api, Postgres db). Covers prerequisites (external proxy network), the root .env credentials, reverse-proxy wiring, bring-up, and verification. Adds the current data model (projects + project_id/parent_id/issued_at columns), the full endpoint list, a "what's stored in SQL today vs Phase 2" table, backups, and the schema-migration caveat (create_all adds tables, not columns). Points to server/README.md for the deep container reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
222
DEPLOYMENT.md
222
DEPLOYMENT.md
@@ -1,81 +1,189 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
The Work Package Suite has two parts:
|
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**.
|
||||||
|
|
||||||
- a **static front end** (plain HTML/CSS/JS — no build step), and
|
The repo already contains everything needed to run it as a Docker stack:
|
||||||
- a **Python API** (FastAPI) backed by **PostgreSQL**, which stores the project
|
`Dockerfile`, `docker-compose.yml`, the `nginx/` config, the front end in
|
||||||
SOPs, Work Packages, and comments so they are shared across users instead of
|
`html/`, and the API in `server/`. The detailed container reference (endpoints,
|
||||||
living in each person's browser.
|
password rotation, day-to-day commands) lives in
|
||||||
|
[`server/README.md`](server/README.md) — this doc is the start-to-finish guide.
|
||||||
|
|
||||||
```
|
```
|
||||||
browser → NGINX ──serves──> static site (index.html, …)
|
[ your TLS reverse proxy / traefik ] ← HTTPS terminates here
|
||||||
└─proxy /api/─> Python API (uvicorn/gunicorn :8000) → PostgreSQL
|
│ (external "proxy" network)
|
||||||
|
┌────▼────┐ internal network ┌──────────┐ ┌────────────┐
|
||||||
|
browser ───────────────────────│ nginx │ ───── /api/ ───────> │ api │ → │ postgres │
|
||||||
|
│ (html/) │ │ FastAPI │ │ (db) │
|
||||||
|
└─────────┘ └──────────┘ └────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Everything runs inside your firewall; the app makes **no outbound internet
|
Everything runs inside your firewall; the app makes **no outbound internet
|
||||||
calls** (the logo and scripts are local and the old Google-Fonts dependency was
|
calls** (logo and scripts are local).
|
||||||
removed).
|
|
||||||
|
|
||||||
## 1. Front end (NGINX)
|
> **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 the `webserver` image (see *Updating* below). The API image
|
||||||
|
> is built from the root `Dockerfile`.
|
||||||
|
|
||||||
Copy the project files to a web root and serve them over HTTPS. The provided
|
---
|
||||||
[`nginx-wp-suite.conf`](nginx-wp-suite.conf) serves the static files and proxies
|
|
||||||
`/api/` to the Python API. Set `server_name`, the `ssl_certificate` paths, and
|
|
||||||
`root`, then `sudo nginx -t && sudo systemctl reload nginx`.
|
|
||||||
|
|
||||||
Serving over real HTTP(S) (not `file://`) also makes the embedded Work Package
|
## 1. Prerequisites
|
||||||
Creator (`<iframe>`) and any browser-side caching behave reliably.
|
|
||||||
|
|
||||||
## 2. API + database
|
- A Linux host with **Docker** and **Docker Compose v2** (`docker compose …`).
|
||||||
|
- An external Docker network named `proxy` that your TLS-terminating reverse
|
||||||
|
proxy also sits on (the compose file marks it `external: true`):
|
||||||
|
```bash
|
||||||
|
docker network create proxy
|
||||||
|
```
|
||||||
|
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.
|
||||||
|
- The repository checked out on the host.
|
||||||
|
|
||||||
Full setup — PostgreSQL, the systemd service, and the endpoint reference — is in
|
## 2. Create the database credentials (`.env`)
|
||||||
[`server/README.md`](server/README.md). In short:
|
|
||||||
|
|
||||||
1. Create the `wpsuite` Postgres database/user.
|
Create a file named `.env` in the **project root** (same folder as
|
||||||
2. `pip install -r server/requirements.txt` into a venv.
|
`docker-compose.yml`). It is git-ignored and must never be committed.
|
||||||
3. Set `DATABASE_URL` and run the API as a systemd service on `127.0.0.1:8000`.
|
|
||||||
4. Tables are created automatically on first start.
|
|
||||||
|
|
||||||
Interactive API docs are at `/api/docs` once it's running.
|
```bash
|
||||||
|
# .env — project root
|
||||||
|
POSTGRES_DB=wpsuite
|
||||||
|
POSTGRES_USER=wpsuite
|
||||||
|
POSTGRES_PASSWORD=<strong-random-password>
|
||||||
|
|
||||||
## 3. Comments / feedback
|
# Must match the POSTGRES_* values above. Host is the compose service name "db".
|
||||||
|
DATABASE_URL=postgresql+psycopg://wpsuite:<strong-random-password>@db:5432/wpsuite
|
||||||
Every feedback surface (home *Leave Feedback*, SOP *Step Comments*, WP *Comments*)
|
|
||||||
posts to `/api/feedback`, which the API stores in the `comments` table. The
|
|
||||||
**Export / Import** buttons remain as an offline fallback — a reviewer can export
|
|
||||||
a JSON file and someone can import/merge it — but with the API running, comments
|
|
||||||
are collected centrally with no manual steps.
|
|
||||||
|
|
||||||
> The earlier Power Automate route is **no longer needed** — comments go straight
|
|
||||||
> to Postgres. If you still want a Power App view, point a Power App at the
|
|
||||||
> Postgres `comments` table via the on-prem data gateway, or have a flow read the
|
|
||||||
> table; no change to this app is required.
|
|
||||||
|
|
||||||
### Comment payload shape
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"app": "Work Package Suite",
|
|
||||||
"page": "/work-package-suite.html",
|
|
||||||
"submittedAt": "2026-06-15T18:20:00.000Z",
|
|
||||||
"type": "sop_step_comment",
|
|
||||||
"name": "J. Park",
|
|
||||||
"text": "Consider adding a fiber WP type",
|
|
||||||
"step": 4
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`type` is one of `home_feedback`, `sop_step_comment`, or `wp_review_comment`. The
|
Generate a strong password with `openssl rand -base64 32`.
|
||||||
API maps `name`/`author` → the comment author and keeps any extra fields in the
|
|
||||||
row's `extra` JSON column.
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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 the `webserver` service (e.g. `"8080:80"`) and terminate TLS at whatever
|
||||||
|
> sits in front of it. The internal `api`/`db` containers should **never** be
|
||||||
|
> published.
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec db psql -U wpsuite -d wpsuite -c "select id, name from projects;"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
## Data model (PostgreSQL)
|
||||||
|
|
||||||
| Table | Holds | Key columns |
|
| Table | Holds | Key columns |
|
||||||
|-------|-------|-------------|
|
|-------|-------|-------------|
|
||||||
| `sops` | project SOP baselines | `name`, `number`, `complete`, `data` (full SOP JSON) |
|
| `projects` | top-level construction projects | `name`, `number`, `client`, `division`, `site`, `sample`, `data` |
|
||||||
| `work_packages` | individual IWPs | `sop_id`, `number`, `subject`, `type`, `status`, `data` (full WP JSON) |
|
| `sops` | project SOP baselines | `project_id` → projects, `name`, `number`, `complete`, `data` (full SOP JSON) |
|
||||||
| `comments` | feedback from any page | `source`, `sop_id`, `wp_id`, `step`, `author`, `text` |
|
| `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` column;
|
The complete client document is stored verbatim in each row's `data` JSON
|
||||||
frequently-listed fields are promoted to real columns for filtering.
|
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`](server/README.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Updating after a change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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 TABLE` before deploying — don't rely on `create_all` for 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`](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).
|
||||||
|
|||||||
Reference in New Issue
Block a user