On this page

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#

CommandCompose filesUse
make updocker-compose.ymlLocal preview, workshops, single-user installations.
make up-dev+ compose.dev.ymlDevelopment with hot reload inside containers.
make devnoneHost dev servers, fastest reload.
Production+ docker-compose.prod.ymlA 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.

InternetTLS certificate on the edge proxy
Edge proxyforwards :80 and :443 to the VM
Caddy (container)TLS termination and path routing
api/api/v1/*, /health, /mcpwebeverything elsekeycloak/auth/*landing page/ and /assets/*, served from disk
apithe one service that owns state
app-dbPostgres metadatadata/Parquet logs and module resultsS3 bucketonly with STORAGE_MODE=s3
Production topology. One public port. Caddy terminates TLS and routes by path; only the API owns state.

Compression 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.

  1. Smoke-test the pipe from outside the VM: curl -sSI https://mate.example.org/health | head -1. If this fails, nothing else will work.

  2. Clone on the VM into a stable directory: git clone … ~/mate.

  3. Secrets and realm in one step: ./infra/bootstrap-vm.sh generates .env secrets, rewrites the realm export's redirect origins and client secret, and starts the stack (unless --no-start).

  4. 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.

  5. 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 api
  6. Verify in order: /health answers · /login renders · sign-in works · a small import completes · Discovery opens · a dashboard loads · MATE AI answers (if configured) · python -m mate.api.storage.migration check passes (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.

TaskHow
Create accountsKeycloak console → Users → Add user, then set a temporary password. The platform creates its own users row on first contact.
Grant adminRealm 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 lookEdit 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#

From a laptop inside the VPN
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.

ChangeNeeds
Application codeup -d --build
A NEXT_PUBLIC_* variable--build: the value is inlined into the client bundle
Any other environment variableup -d — a recreate, not a restart
CaddyfileRestart of the proxy container
A realm settingA change in the Keycloak console, not the JSON
The Keycloak login themeRestart of the keycloak container; returning browsers pick up new CSS within an hour
A module's dependency blockNothing: 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.

Rolling back
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.