From ffcaa571d12ae7aed0749f39092ebb26071e8594 Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 13:46:43 -0500 Subject: [PATCH 1/8] Add API to production --- Dockerfile | 8 ++ server/README.md | 219 ++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 197 insertions(+), 30 deletions(-) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..00d7907 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.12-slim +WORKDIR /app +COPY server/requirements.txt ./server/ +RUN pip install --no-cache-dir -r server/requirements.txt +COPY server/ ./server/ +EXPOSE 8000 +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", \ + "-b", "0.0.0.0:8000", "--workers", "2", "server.app:app"] \ No newline at end of file diff --git a/server/README.md b/server/README.md index d9343f1..d6075bc 100644 --- a/server/README.md +++ b/server/README.md @@ -6,7 +6,7 @@ to this service. ``` browser → NGINX ──serves──> static site (index.html, …) - └─proxy /api/─> this API (uvicorn/gunicorn :8000) → PostgreSQL + └─proxy /api/─> api container (:8000) → db container (postgres) ``` ## Endpoints @@ -31,6 +31,8 @@ Interactive docs once running: **`/api/docs`**. The full client document is stored in each row's `data` (JSON) column; common fields (name, number, status, …) are promoted to columns for listing/filtering. +--- + ## Local dev ```bash @@ -45,42 +47,193 @@ Then open http://localhost:8000/api/docs. > Run uvicorn/gunicorn from the **project root** (the folder that contains the > `server/` directory), because the import path is `server.app:app`. -## PostgreSQL setup (production) +--- + +## Production — Docker Compose + +This is the recommended production setup. Three containers run in an isolated +internal network; only NGINX is exposed to the outside via the external `proxy` +network. -```sql -CREATE DATABASE wpsuite; -CREATE USER wpsuite WITH PASSWORD 'CHANGE_ME'; -GRANT ALL PRIVILEGES ON DATABASE wpsuite TO wpsuite; ``` -Tables are created automatically on first startup. (For future schema changes, -introduce Alembic migrations rather than editing tables by hand.) - -## Run in production (gunicorn + systemd) - -`/etc/systemd/system/wp-suite-api.service`: - -```ini -[Unit] -Description=Work Package Suite API -After=network.target - -[Service] -User=www-data -WorkingDirectory=/opt/wp-suite -Environment="DATABASE_URL=postgresql+psycopg://wpsuite:CHANGE_ME@localhost:5432/wpsuite" -ExecStart=/opt/wp-suite/.venv/bin/gunicorn -k uvicorn.workers.UvicornWorker -b 127.0.0.1:8000 server.app:app -Restart=always - -[Install] -WantedBy=multi-user.target +[external proxy network] + │ + ┌────▼────┐ internal network ┌──────────┐ ┌────────┐ + │ nginx │ ───────────────────> │ api │ → │ db │ + └─────────┘ └──────────┘ └────────┘ ``` +### 1. Create the credentials file + +Create `.env` in the **project root** (same directory as `docker-compose.yml`). +This file is never committed — add it to `.gitignore`. + ```bash -sudo systemctl daemon-reload -sudo systemctl enable --now wp-suite-api +# .env — project root +POSTGRES_DB=wpsuite +POSTGRES_USER=wpsuite +POSTGRES_PASSWORD= + +# Must match POSTGRES_* above; hostname is the compose service name "db" +DATABASE_URL=postgresql+psycopg://wpsuite:@db:5432/wpsuite ``` -NGINX already proxies `/api/` to `127.0.0.1:8000` (see `nginx-wp-suite.conf`). +Generate a strong password: +```bash +openssl rand -base64 32 +``` + +### 2. Add the Dockerfile + +Create `Dockerfile` in the **project root**: + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY server/requirements.txt ./server/ +RUN pip install --no-cache-dir -r server/requirements.txt +COPY server/ ./server/ +EXPOSE 8000 +CMD ["gunicorn", "-k", "uvicorn.workers.UvicornWorker", \ + "-b", "0.0.0.0:8000", "--workers", "2", "server.app:app"] +``` + +### 3. Update the NGINX site config + +The API is no longer at `127.0.0.1:8000` — it is the `api` container. +Update the `/api/` proxy block in your nginx conf (e.g. `nginx/conf.d/wp-suite.conf`): + +```nginx +location /api/ { + proxy_pass http://api:8000; # ← service name, not localhost + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 5m; +} +``` + +### 4. docker-compose.yml + +Replace your existing `docker-compose.yml` with: + +```yaml +services: + + webserver: + image: nginx:alpine + container_name: nginx_webserver + volumes: + - ./html:/usr/share/nginx/html:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./logs:/var/log/nginx + restart: unless-stopped + depends_on: + api: + condition: service_started + networks: + - proxy # external — reachable by your reverse proxy / traefik + - internal # needs a path to the api container + + api: + build: . + container_name: wp_api + env_file: .env # loads DATABASE_URL + restart: unless-stopped + depends_on: + db: + condition: service_healthy # waits for postgres to accept connections + networks: + - internal + + db: + image: postgres:16-alpine + container_name: wp_db + env_file: .env # loads POSTGRES_DB / USER / PASSWORD + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - internal + +volumes: + pgdata: + +networks: + proxy: + name: proxy + external: true + internal: + internal: true # no outbound internet access from api/db +``` + +### 5. First-time startup + +```bash +# Build the api image and start all containers +docker compose up -d --build + +# Confirm all three containers are running +docker compose ps + +# Tail logs (Ctrl-C to stop following) +docker compose logs -f api +``` + +Tables are created automatically on first API startup — no manual `CREATE TABLE` +needed. + +### Authentication notes + +**Postgres → API authentication** is handled entirely through `DATABASE_URL` in +`.env`. The `db` container uses `POSTGRES_USER` / `POSTGRES_PASSWORD` to +initialise the database on first run; the `api` container uses the matching +credentials in `DATABASE_URL` to connect. Neither credential ever appears in the +compose file itself. + +**Network isolation**: the `db` container is on the `internal` network only — +it has no port exposed to the host and is unreachable from outside the compose +stack. Only the `api` container can open a connection to it. + +**Changing the password**: update both `POSTGRES_PASSWORD` and the password +in `DATABASE_URL` in `.env`, then: +```bash +# Stop api first (db must keep running to accept the ALTER USER command) +docker compose stop api +docker compose exec db psql -U wpsuite -c "ALTER USER wpsuite PASSWORD 'new-password';" +docker compose start api +``` + +### Day-to-day operations + +```bash +# Rebuild api after a code change +docker compose up -d --build api + +# View postgres data directly +docker compose exec db psql -U wpsuite -d wpsuite + +# Take a database backup +docker compose exec db pg_dump -U wpsuite wpsuite > backup-$(date +%F).sql + +# Restore from backup +docker compose exec -T db psql -U wpsuite -d wpsuite < backup-2025-01-01.sql + +# Stop everything (data volume is preserved) +docker compose down + +# Stop everything AND delete all data +docker compose down -v +``` + +--- ## Quick test @@ -91,3 +244,9 @@ curl -X POST http://127.0.0.1:8000/api/comments \ curl http://127.0.0.1:8000/api/comments ``` + +Or via the nginx proxy (replace with your hostname): + +```bash +curl https://wp-suite.company.local/api/health +``` From b0a3d744123ad241dd75f04473877d7246730cd6 Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:02:52 -0500 Subject: [PATCH 2/8] Add docker compose --- docker-compose.yml | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 docker-compose.yml diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c9f1e88 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,51 @@ +services: + + webserver: + image: nginx:alpine + container_name: nginx_webserver + volumes: + - ./html:/usr/share/nginx/html:ro + - ./nginx/conf.d:/etc/nginx/conf.d:ro + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./logs:/var/log/nginx + restart: unless-stopped + depends_on: + api: + condition: service_started + networks: + - proxy # external — reachable by your reverse proxy / traefik + - internal # needs a path to the api container + + api: + build: . + container_name: wp_api + restart: unless-stopped + depends_on: + db: + condition: service_healthy # waits for postgres to accept connections + networks: + - internal + + db: + image: postgres:16-alpine + container_name: wp_db + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - internal + +volumes: + pgdata: + +networks: + proxy: + name: proxy + external: true + internal: + internal: true # no outbound internet access from api/db \ No newline at end of file From 65996c2c0a2b20694cbfc479b91b9e595a33e1f5 Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:25:18 -0500 Subject: [PATCH 3/8] troubleshoot docker-compose --- docker-compose.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c9f1e88..eb67657 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,11 +32,11 @@ services: volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - interval: 10s - timeout: 5s - retries: 5 + # healthcheck: + # test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + # interval: 10s + # timeout: 5s + # retries: 5 networks: - internal From 8598606165758f1bf33f912424a8ace2b4f07a02 Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:31:56 -0500 Subject: [PATCH 4/8] fix docker compose --- docker-compose.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index eb67657..e3379b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,6 +19,8 @@ services: api: build: . container_name: wp_api + environment: + DATABASE_URL: ${DATABASE_URL} restart: unless-stopped depends_on: db: @@ -29,14 +31,18 @@ services: db: image: postgres:16-alpine container_name: wp_db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped - # healthcheck: - # test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] - # interval: 10s - # timeout: 5s - # retries: 5 + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 networks: - internal From 960b4a4b94b71f6fef972d542d9e37aa68e4ae60 Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:42:25 -0500 Subject: [PATCH 5/8] nginx updates --- .gitignore | 3 +++ docker-compose.yml | 1 - nginx/conf.d/wp-suite.conf | 25 +++++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 nginx/conf.d/wp-suite.conf diff --git a/.gitignore b/.gitignore index 8be8a9d..0c5f277 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ venv/ # Local SQLite dev database *.db wpsuite.db + +# Runtime directories (created by containers) +logs/ diff --git a/docker-compose.yml b/docker-compose.yml index e3379b4..45f1ae9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,6 @@ services: volumes: - ./html:/usr/share/nginx/html:ro - ./nginx/conf.d:/etc/nginx/conf.d:ro - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./logs:/var/log/nginx restart: unless-stopped depends_on: diff --git a/nginx/conf.d/wp-suite.conf b/nginx/conf.d/wp-suite.conf new file mode 100644 index 0000000..22b8996 --- /dev/null +++ b/nginx/conf.d/wp-suite.conf @@ -0,0 +1,25 @@ +# Work Package Suite — NGINX site config +# This container sits behind an external reverse proxy that handles SSL. +# It listens on port 80 (plain HTTP on the internal Docker network). + +server { + listen 80; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + # Proxy /api/ to the FastAPI container (service name "api" on the internal network) + location /api/ { + proxy_pass http://api:8000; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + client_max_body_size 5m; + } +} From fd668f0ea2dffef3c2b2285cfda6efee14b1925c Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:47:24 -0500 Subject: [PATCH 6/8] Move static files into html/ for Docker bind mount --- favicon.ico => html/favicon.ico | Bin feedback-config.js => html/feedback-config.js | 0 index.html => html/index.html | 0 .../prime-controls-logo.jpg | Bin theme-light.css => html/theme-light.css | 0 .../work-package-suite-app.js | 0 .../work-package-suite-styles.css | 0 .../work-package-suite.html | 0 wp-creation-app.js => html/wp-creation-app.js | 0 .../wp-creation-index.html | 0 .../wp-creation-styles.css | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename favicon.ico => html/favicon.ico (100%) rename feedback-config.js => html/feedback-config.js (100%) rename index.html => html/index.html (100%) rename prime-controls-logo.jpg => html/prime-controls-logo.jpg (100%) rename theme-light.css => html/theme-light.css (100%) rename work-package-suite-app.js => html/work-package-suite-app.js (100%) rename work-package-suite-styles.css => html/work-package-suite-styles.css (100%) rename work-package-suite.html => html/work-package-suite.html (100%) rename wp-creation-app.js => html/wp-creation-app.js (100%) rename wp-creation-index.html => html/wp-creation-index.html (100%) rename wp-creation-styles.css => html/wp-creation-styles.css (100%) diff --git a/favicon.ico b/html/favicon.ico similarity index 100% rename from favicon.ico rename to html/favicon.ico diff --git a/feedback-config.js b/html/feedback-config.js similarity index 100% rename from feedback-config.js rename to html/feedback-config.js diff --git a/index.html b/html/index.html similarity index 100% rename from index.html rename to html/index.html diff --git a/prime-controls-logo.jpg b/html/prime-controls-logo.jpg similarity index 100% rename from prime-controls-logo.jpg rename to html/prime-controls-logo.jpg diff --git a/theme-light.css b/html/theme-light.css similarity index 100% rename from theme-light.css rename to html/theme-light.css diff --git a/work-package-suite-app.js b/html/work-package-suite-app.js similarity index 100% rename from work-package-suite-app.js rename to html/work-package-suite-app.js diff --git a/work-package-suite-styles.css b/html/work-package-suite-styles.css similarity index 100% rename from work-package-suite-styles.css rename to html/work-package-suite-styles.css diff --git a/work-package-suite.html b/html/work-package-suite.html similarity index 100% rename from work-package-suite.html rename to html/work-package-suite.html diff --git a/wp-creation-app.js b/html/wp-creation-app.js similarity index 100% rename from wp-creation-app.js rename to html/wp-creation-app.js diff --git a/wp-creation-index.html b/html/wp-creation-index.html similarity index 100% rename from wp-creation-index.html rename to html/wp-creation-index.html diff --git a/wp-creation-styles.css b/html/wp-creation-styles.css similarity index 100% rename from wp-creation-styles.css rename to html/wp-creation-styles.css From 362aa633ed13a9b82b2c90979017303b8d8c00fa Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 14:57:54 -0500 Subject: [PATCH 7/8] Build nginx image with static files baked in; remove host bind mounts --- docker-compose.yml | 9 +++++---- nginx/Dockerfile | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 nginx/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 45f1ae9..def674d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,12 @@ services: webserver: - image: nginx:alpine + build: + context: . + dockerfile: nginx/Dockerfile container_name: nginx_webserver volumes: - - ./html:/usr/share/nginx/html:ro - - ./nginx/conf.d:/etc/nginx/conf.d:ro - - ./logs:/var/log/nginx + - nginx_logs:/var/log/nginx restart: unless-stopped depends_on: api: @@ -47,6 +47,7 @@ services: volumes: pgdata: + nginx_logs: networks: proxy: diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..fc95796 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,3 @@ +FROM nginx:alpine +COPY nginx/conf.d/wp-suite.conf /etc/nginx/conf.d/wp-suite.conf +COPY html/ /usr/share/nginx/html/ From a37cf14e89047d4aeece3e1883318d64f2dfa5ed Mon Sep 17 00:00:00 2001 From: Tim Shepardson Date: Mon, 15 Jun 2026 15:13:54 -0500 Subject: [PATCH 8/8] fix nginx.conf --- nginx/Dockerfile | 1 + nginx/nginx.conf | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 nginx/nginx.conf diff --git a/nginx/Dockerfile b/nginx/Dockerfile index fc95796..7508bf5 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,3 +1,4 @@ FROM nginx:alpine COPY nginx/conf.d/wp-suite.conf /etc/nginx/conf.d/wp-suite.conf +COPY nginx/nginx.conf /etc/nginx/nginx.conf COPY html/ /usr/share/nginx/html/ diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..37e9521 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,17 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + keepalive_timeout 65; + include /etc/nginx/conf.d/*.conf; +} \ No newline at end of file