---
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
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9