Deployment
Two deployments are supported: a local stack on one machine, and a production deployment behind a proxy with TLS. Both use the same compose file plus an overlay.
Deployment modes#
| Command | Compose files | Use |
|---|---|---|
make up | docker-compose.yml | Local preview, workshops, single-user installations. |
make up-dev | + compose.dev.yml | Development with hot reload inside containers. |
make dev | none | Host dev servers, fastest reload. |
| Production | + docker-compose.prod.yml | A VM behind an edge proxy, with TLS and resource caps. |
The production overlay adds a Caddy container as the only published port, resets the app ports to unpublished, points every URL at the public origin, serves the landing page from ./landing, enables server-side sessions, applies memory limits per service, and caps container logs (10m × 3 files). A one-shot keycloak-config service applies realm hardening on every deploy.
:80 and :443 to the VMapi/api/v1/*, /health, /mcpwebeverything elsekeycloak/auth/*landing page/ and /assets/*, served from diskapithe one service that owns state
app-dbPostgres metadatadata/Parquet logs and module resultsS3 bucketonly with STORAGE_MODE=s3Compression excludes text/event-stream on purpose, so streaming responses are never buffered. Security headers (HSTS, nosniff, referrer policy) and a report-only CSP are set on the web app.
Production runbook#
Each step rules out one layer, so a failure surfaces where it occurs.
Smoke-test the pipe from outside the VM:
curl -sSI https://mate.example.org/health | head -1. If this fails, nothing else will work.Clone on the VM into a stable directory:
git clone … ~/mate.Secrets and realm in one step:
./infra/bootstrap-vm.shgenerates.envsecrets, rewrites the realm export's redirect origins and client secret, and starts the stack (unless--no-start).Fill
.env:AUTH_SECRET,KEYCLOAK_CLIENT_SECRET,NEXT_PUBLIC_API_URL,AUTH_URL,KEYCLOAK_ISSUER,API_BASE_URL, and the tracking policy. Full list: Configuration.Bring it up:
Terminal docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f apiVerify in order:
/healthanswers ·/loginrenders · sign-in works · a small import completes · Discovery opens · a dashboard loads · MATE AI answers (if configured) ·python -m mate.api.storage.migration checkpasses (if S3).
Optional components: the CV4CDD model files (uploaded on the module's settings page), the Neo4j sidecar (compose profile graph), and S3 storage.
Identity setup#
The realm JSON imports only into an empty Keycloak database; everything after that is a console change.
| Task | How |
|---|---|
| Create accounts | Keycloak console → Users → Add user, then set a temporary password. The platform creates its own users row on first contact. |
| Grant admin | Realm roles → admin → Assign role. The seeded account deliberately has no admin role. |
| Broker an institutional IdP | ./infra/keycloak/configure-university-idp.sh, then set KEYCLOAK_IDP_HINT so users skip Keycloak's form. Keep the local form as break-glass. |
| Admin REST client (user deletion) | ./infra/keycloak/configure-admin-client.sh |
| MCP OAuth client | ./infra/keycloak/configure-mcp-client.sh |
| Change how Keycloak's own pages look | Edit the mate theme in infra/keycloak/themes/mate. Every deploy re-applies loginTheme=mate, so do not switch it in the console. |
Deleting a user from Admin → Users purges their logs, results, jobs, dashboards, tokens, and settings, removes the Keycloak identity, and deletes their S3 prefix. This is intentionally irreversible.
Updating a deployment#
make deploy # pushes the branch, resets the server clone, rebuilds, health-checks
make deploy reads scripts/deploy.env (DEPLOY_HOST, DEPLOY_USER, optional DEPLOY_PORT, DEPLOY_DIR, DEPLOY_BRANCH, DEPLOY_PUBLIC_URL), pushes the current branch, resets the server's clone to it, rebuilds with the production overlay, prunes dangling images, and polls /health until the new version answers.
| Change | Needs |
|---|---|
| Application code | up -d --build |
A NEXT_PUBLIC_* variable | --build: the value is inlined into the client bundle |
| Any other environment variable | up -d — a recreate, not a restart |
| Caddyfile | Restart of the proxy container |
| A realm setting | A change in the Keycloak console, not the JSON |
| The Keycloak login theme | Restart of the keycloak container; returning browsers pick up new CSS within an hour |
| A module's dependency block | Nothing: the loader re-materialises on boot |
Migrations run on API boot (alembic upgrade head), so a deploy migrates itself. There is no rolling deployment — a restart interrupts running jobs, which are marked failed at the next boot while logs left mid-import are reconciled.
git log --oneline -5 && git reset --hard <sha>
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
Rolling back the code does not roll back migrations. If the release included a destructive schema change, restore the metadata snapshot first.