Skip to main content

Deployment

Compose files​

Local (loads docker-compose.override.yml, which publishes Postgres on 55432):

docker compose up -d --build

Production — base file only, so the Postgres host port is not published:

docker compose -f docker-compose.yml up -d --build
ServicePort
Portal3000
Gateway8080
Docs3002
Databaseinternal only in production
Redisinternal only

Hard requirements​

  • AUTH_DEV_BYPASS must be unset or false in production .env. Production portal builds also ignore the flag if it is leftover true, so /login never shows the any-password demo form. After changing auth, rebuild portal (docker compose -f docker-compose.prod.yml up -d --build portal)
  • TLS: gateway and portal speak plain HTTP and must sit behind a TLS-terminating reverse proxy. The gateway streams SSE — disable proxy response buffering and allow long response times for /v1/chat/completions
  • Redis has no authentication in compose. Keep it on the internal network, or set a password/TLS via REDIS_URL
  • Do not reuse any values from a developer's local .env

OpenAPI artifact (gateway image)​

Before building the zeagate image, generate the embedded OpenAPI JSON from the YAML source (or pull a commit that already includes gateway/openapi/openapi.json):

node scripts/generate-api-contract.mjs

Portal production builds run this automatically via npm run prebuild. The portal image build uses the repository root as Docker context (portal/Dockerfile) so scripts/ and openapi/ are available inside the builder.

Database migrations​

The portal applies pending database migrations automatically on startup. A normal deploy needs no manual database step.

If you see a missing-column (or missing-table) error after a deploy, pull the latest release — it must include the matching migration — and redeploy. Never patch the database by hand. Migrations are forward-only; rollbacks ship as new migrations.

Probes​

EndpointUse
GET /health/livelinessLiveness — process is up
GET /health/readinessReadiness — Postgres, and Redis when REDIS_URL is set

Invite emails (SMTP)​

SMTP_HOST=smtp.office365.com
SMTP_PORT=587
SMTP_USER=zeallm@zealogics.com
SMTP_PASS=<app password>
SMTP_FROM=zeallm@zealogics.com

Without SMTP_HOST, invites still create the account.

Weekly digest email​

Every Monday the portal can email each user a short summary of their week: developers get their keys' spend and alerts, approvers what is waiting on them, platform admins platform spend, top teams and health. Users with nothing to report are skipped, and anyone can turn it off in Settings → Profile.

The digest is sent by POST /api/cron/weekly-digest. The endpoint only accepts Authorization: Bearer $CRON_SECRET (401 otherwise, 503 while CRON_SECRET is unset) and needs SMTP. It is safe to call more than once a week: each user's last-sent ISO week is recorded, so a retry never sends twice.

  1. Set CRON_SECRET (32+ random characters) and the SMTP_* variables in .env.

  2. Start the scheduler service, which calls the endpoint every Monday at 08:00 in ZEALLM_TIMEZONE:

    docker compose -f docker-compose.yml --profile weekly-digest up -d --build

To use another scheduler instead (system cron, Kubernetes CronJob, Azure Logic Apps…), call the endpoint yourself:

# Mondays 08:00
0 8 * * 1 curl -fsS -X POST -H "Authorization: Bearer $CRON_SECRET" https://<portal-host>/api/cron/weekly-digest

Add ?dryRun=1 to build every digest and return the counts without sending anything. The response is { sent, skipped, failed, ... }; one failed email never stops the rest, and failed users are retried on the next call.

Microsoft Entra ID SSO​

  1. Azure portal → App registration (single tenant)
  2. Web redirect URI: https://<host>/api/auth/callback/microsoft-entra-id (local: http://localhost:3000/api/auth/callback/microsoft-entra-id)
  3. Set AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET, AZURE_AD_TENANT_ID, ZEALLM_ALLOWED_DOMAIN, ZEALLM_ADMIN_EMAILS
  4. AUTH_DEV_BYPASS=false in the production .env, then rebuild portal

The “Continue with Microsoft” button appears once all three AZURE_AD_* vars are set. Default openid profile email scopes are enough. Production never registers the local demo credentials provider.

Load testing​

A load-test script lives in loadtest/. Point it at the gateway URL, a revealed virtual key, and a model the key can access.