Merge branch 'main' into feat/wp-discipline-split-dashboard
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,3 +11,6 @@ venv/
|
||||
# Local SQLite dev database
|
||||
*.db
|
||||
wpsuite.db
|
||||
|
||||
# Runtime directories (created by containers)
|
||||
logs/
|
||||
|
||||
8
Dockerfile
Normal file
8
Dockerfile
Normal file
@@ -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"]
|
||||
57
docker-compose.yml
Normal file
57
docker-compose.yml
Normal file
@@ -0,0 +1,57 @@
|
||||
services:
|
||||
|
||||
webserver:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: nginx/Dockerfile
|
||||
container_name: nginx_webserver
|
||||
volumes:
|
||||
- nginx_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
|
||||
environment:
|
||||
DATABASE_URL: ${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
|
||||
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
|
||||
networks:
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
nginx_logs:
|
||||
|
||||
networks:
|
||||
proxy:
|
||||
name: proxy
|
||||
external: true
|
||||
internal:
|
||||
internal: true # no outbound internet access from api/db
|
||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
4
nginx/Dockerfile
Normal file
4
nginx/Dockerfile
Normal file
@@ -0,0 +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/
|
||||
25
nginx/conf.d/wp-suite.conf
Normal file
25
nginx/conf.d/wp-suite.conf
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
17
nginx/nginx.conf
Normal file
17
nginx/nginx.conf
Normal file
@@ -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;
|
||||
}
|
||||
219
server/README.md
219
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=<strong-random-password>
|
||||
|
||||
# Must match POSTGRES_* above; hostname is the compose service name "db"
|
||||
DATABASE_URL=postgresql+psycopg://wpsuite:<strong-random-password>@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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user