--- category: reference tags: - robot-wtf - v1 - deployment last_updated: 2026-03-15 --- # V1-3: Gunicorn Configuration, Systemd Units, and Deployment **Branch:** `feat/v1-3-gunicorn-systemd` in `robot.wtf` repo **Commit:** `5eb51c6` ## Deliverables ### Entry Points (app/) | File | Service | Port | Server | Description | |------|---------|------|--------|-------------| | `wsgi.py` | Otterwiki | 8000 | Gunicorn | Wraps Flask app in TenantResolver + ManagementMiddleware. Stub when otterwiki not installed. | | `api_server.py` | Platform API | 8002 | Gunicorn | ManagementMiddleware + `/api/internal/check-slug` for Caddy on-demand TLS + static file serving. | | `auth_server.py` | Auth | 8003 | Gunicorn | ATProto OAuth stubs (501). Serves client-metadata.json, AS metadata, JWKS. | | `mcp_entry.py` | MCP Sidecar | 8001 | Uvicorn | FastMCP stub (ASGI). Serves `/.well-known/oauth-protected-resource`. | ### Shared Configuration - `app/gunicorn.conf.py` — Bind from `GUNICORN_BIND` env var, workers = min(2*CPU+1, 4), timeout 30s, access log to stdout. ### Systemd Units (ansible/roles/deploy/files/) All services run as `robot` user, `WorkingDirectory=/srv/app/src`. - `robot-otterwiki.service` — Gunicorn, port 8000, MULTI_TENANT=true - `robot-api.service` — Gunicorn, port 8002 - `robot-auth.service` — Gunicorn, port 8003 - `robot-mcp.service` — Uvicorn, port 8001 ### Ansible Deploy Role `ansible/roles/deploy/` with tasks + handlers: - Syncs `app/` to `/srv/app/src/app/` via rsync - Installs `requirements.txt` into `/srv/app/venv` - Copies systemd units, enables and starts all four services - Handlers restart services on code change - **TODO:** otterwiki pip install from fork (placeholder comments) ### Requirements Added - `flask>=3.0.0` — needed by api_server, auth_server, wsgi stub - `uvicorn>=0.29.0` — needed by MCP sidecar ## Environment Variables | Variable | Used By | Default | |----------|---------|---------| | MULTI_TENANT | wsgi.py | (unset = disabled) | | ROBOT_DB_PATH | wsgi, api | /srv/data/robot.db | | SIGNING_KEY_PATH | wsgi, api, auth | /srv/data/signing_key.pem | | PLATFORM_DOMAIN | all | robot.wtf | | WIKI_BASE | wsgi | /srv/data/wikis | | GUNICORN_BIND | gunicorn.conf | 0.0.0.0:8000 | | ROBOT_STATIC_DIR | api | /srv/static | | MCP_PORT | mcp | 8001 | ## Testing - All 55 existing tests pass (no regressions) - auth_server: stub routes return 501, metadata routes return 200 with valid JSON - mcp_entry: ASGI stub serves oauth-protected-resource (200) and default (501) - wsgi.py: loads stub Flask app when otterwiki not installed - api_server: check-slug returns 404 for unknown domains/slugs - Systemd units validated: correct User, WorkingDirectory, ExecStart, ports - Ansible YAML validated: 7 tasks, 2 handlers