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 +```