Blame
|
1 | --- |
||||||
| 2 | category: reference |
|||||||
| 3 | tags: |
|||||||
| 4 | - robot-wtf |
|||||||
| 5 | - v1 |
|||||||
| 6 | - deployment |
|||||||
| 7 | last_updated: 2026-03-15 |
|||||||
| 8 | --- |
|||||||
| 9 | ||||||||
| 10 | # V1-3: Gunicorn Configuration, Systemd Units, and Deployment |
|||||||
| 11 | ||||||||
| 12 | **Branch:** `feat/v1-3-gunicorn-systemd` in `robot.wtf` repo |
|||||||
| 13 | **Commit:** `5eb51c6` |
|||||||
| 14 | ||||||||
| 15 | ## Deliverables |
|||||||
| 16 | ||||||||
| 17 | ### Entry Points (app/) |
|||||||
| 18 | ||||||||
| 19 | | File | Service | Port | Server | Description | |
|||||||
| 20 | |------|---------|------|--------|-------------| |
|||||||
| 21 | | `wsgi.py` | Otterwiki | 8000 | Gunicorn | Wraps Flask app in TenantResolver + ManagementMiddleware. Stub when otterwiki not installed. | |
|||||||
| 22 | | `api_server.py` | Platform API | 8002 | Gunicorn | ManagementMiddleware + `/api/internal/check-slug` for Caddy on-demand TLS + static file serving. | |
|||||||
| 23 | | `auth_server.py` | Auth | 8003 | Gunicorn | ATProto OAuth stubs (501). Serves client-metadata.json, AS metadata, JWKS. | |
|||||||
| 24 | | `mcp_entry.py` | MCP Sidecar | 8001 | Uvicorn | FastMCP stub (ASGI). Serves `/.well-known/oauth-protected-resource`. | |
|||||||
| 25 | ||||||||
| 26 | ### Shared Configuration |
|||||||
| 27 | ||||||||
| 28 | - `app/gunicorn.conf.py` — Bind from `GUNICORN_BIND` env var, workers = min(2*CPU+1, 4), timeout 30s, access log to stdout. |
|||||||
| 29 | ||||||||
| 30 | ### Systemd Units (ansible/roles/deploy/files/) |
|||||||
| 31 | ||||||||
| 32 | All services run as `robot` user, `WorkingDirectory=/srv/app/src`. |
|||||||
| 33 | ||||||||
| 34 | - `robot-otterwiki.service` — Gunicorn, port 8000, MULTI_TENANT=true |
|||||||
| 35 | - `robot-api.service` — Gunicorn, port 8002 |
|||||||
| 36 | - `robot-auth.service` — Gunicorn, port 8003 |
|||||||
| 37 | - `robot-mcp.service` — Uvicorn, port 8001 |
|||||||
| 38 | ||||||||
| 39 | ### Ansible Deploy Role |
|||||||
| 40 | ||||||||
| 41 | `ansible/roles/deploy/` with tasks + handlers: |
|||||||
| 42 | - Syncs `app/` to `/srv/app/src/app/` via rsync |
|||||||
| 43 | - Installs `requirements.txt` into `/srv/app/venv` |
|||||||
| 44 | - Copies systemd units, enables and starts all four services |
|||||||
| 45 | - Handlers restart services on code change |
|||||||
| 46 | - **TODO:** otterwiki pip install from fork (placeholder comments) |
|||||||
| 47 | ||||||||
| 48 | ### Requirements Added |
|||||||
| 49 | ||||||||
| 50 | - `flask>=3.0.0` — needed by api_server, auth_server, wsgi stub |
|||||||
| 51 | - `uvicorn>=0.29.0` — needed by MCP sidecar |
|||||||
| 52 | ||||||||
| 53 | ## Environment Variables |
|||||||
| 54 | ||||||||
| 55 | | Variable | Used By | Default | |
|||||||
| 56 | |----------|---------|---------| |
|||||||
| 57 | | MULTI_TENANT | wsgi.py | (unset = disabled) | |
|||||||
| 58 | | ROBOT_DB_PATH | wsgi, api | /srv/data/robot.db | |
|||||||
| 59 | | SIGNING_KEY_PATH | wsgi, api, auth | /srv/data/signing_key.pem | |
|||||||
| 60 | | PLATFORM_DOMAIN | all | robot.wtf | |
|||||||
| 61 | | WIKI_BASE | wsgi | /srv/data/wikis | |
|||||||
| 62 | | GUNICORN_BIND | gunicorn.conf | 0.0.0.0:8000 | |
|||||||
| 63 | | ROBOT_STATIC_DIR | api | /srv/static | |
|||||||
| 64 | | MCP_PORT | mcp | 8001 | |
|||||||
| 65 | ||||||||
| 66 | ## Testing |
|||||||
| 67 | ||||||||
| 68 | - All 55 existing tests pass (no regressions) |
|||||||
| 69 | - auth_server: stub routes return 501, metadata routes return 200 with valid JSON |
|||||||
| 70 | - mcp_entry: ASGI stub serves oauth-protected-resource (200) and default (501) |
|||||||
| 71 | - wsgi.py: loads stub Flask app when otterwiki not installed |
|||||||
| 72 | - api_server: check-slug returns 404 for unknown domains/slugs |
|||||||
| 73 | - Systemd units validated: correct User, WorkingDirectory, ExecStart, ports |
|||||||
| 74 | - Ansible YAML validated: 7 tasks, 2 handlers |
|||||||