Deployment
Magic Core is orchestrated with docker-compose. Two compose files cover two purposes:
| File | Purpose | Services |
|---|---|---|
docker-compose.yml | local dev infrastructure | Postgres + Redis only (apps run via pnpm) |
docker-compose.prod.yml | production | Postgres + Redis + cliproxy + platform-api + agent-service + relay + keeper + gateway + nginx |
Production topology
:80
┌──────────────┐
client ──▶│ nginx │ forwards everything to gateway
└──────┬───────┘
▼
┌──────────────┐
│ gateway │ routes by path prefix (health-aware)
│ :3110 │
└──────┬───────┘
┌────────────┬─────┴──────┬────────────┐
▼ ▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│platform- │ │ agent- │ │ relay │ │ keeper │
│api :3111 │ │svc :3112 │ │ :3113 │ │ :3114 │
└────┬─────┘ └────┬─────┘ └───┬────┘ └───┬────┘
│ │ ▼ ▼
│ │ ┌─────────────────────┐
│ └─────▶│ cliproxy :8317 │ account pool (internal)
│ └─────────────────────┘
└───────────┬───────────────┘
▼
┌──────────────────┐
│ Postgres + Redis │
└──────────────────┘| Service | Port | Public | Notes |
|---|---|---|---|
| nginx | 80 | ✅ | the single entry point; reverse-proxies everything to gateway |
| gateway | 3110 | via nginx | routes by path prefix and verifies edge identity |
| platform-api | 3111 | via nginx | CRUD + billing + admin console; the only migration writer |
| agent-service | 3112 | via nginx | AI / SSE / realtime WS |
| relay | 3113 | via nginx | OpenAI-compatible /v1 over the account pool |
| keeper | 3114 | via nginx | account-pool worker + /api/admin/keeper admin API |
| cliproxy | 8317 | ❌ | CLIProxyAPI account pool; internal-only, no published ports |
| Postgres / Redis | 5432 / 6379 | ❌ | data / cache |
Magic Core images are published to GHCR: ghcr.io/linmoqc/magic-core/<service>:${MAGIC_CORE_IMAGE_TAG:-main}. cliproxy uses the upstream image ghcr.io/router-for-me/cliproxyapi. Each service has CPU / memory limits (agent-service is the heaviest).
Routing
nginx (deploy/nginx.conf) is deliberately thin: a single location / proxies everything to gateway, with proxy_buffering off and proxy_read_timeout 3600s so SSE / long streams aren’t cut off. All path routing lives in the gateway (routing.constants.ts), which is health-aware:
/v1and/v1/*→ relay (RELAY_SERVICE_URL)/api/admin/keeperand/api/admin/keeper/*→ keeper (KEEPER_SERVICE_URL)/api/(chat|graph|agent|pdf|translate|interview)→ agent-service, upgrading WebSocket for/api/interview/realtime/:sessionId- everything else under
/api/(incl./api/docs) → platform-api
relay and keeper are not hard depends_on of the gateway. The gateway is health-aware and restart: always, so a late or absent relay/keeper never blocks the core edge — those routes just report unavailable until the service is up.
cliproxy (the account pool)
cliproxy is the CLIProxyAPI reverse proxy over Codex/Claude/Gemini accounts. It is internal-only (no published ports), reached over magic-network:
- relay forwards
/v1traffic toRELAY_UPSTREAM_BASE_URL(defaulthttp://cliproxy:8317/v1). - keeper and platform-api reach its management API at
CLIPROXY_MANAGEMENT_BASE_URL(defaulthttp://cliproxy:8317).
OAuth tokens are logged in manually once and persist in the cliproxy_auth volume; its secret-free config template is rendered to runtime/ before start.
Migration ownership (single writer)
Only platform-api runs prisma migrate deploy at start:prod. agent-service, relay, and keeper deliberately do not run migrations — all apps share one Postgres, and multiple writers would race at boot.
When self-hosting, never let a second app run migrations. In compose the other apps wait for platform-api to start and the infrastructure services to be healthy.
Environment (single .env, interpolated)
Production is driven by one .env in the compose directory. Compose interpolates it into each service’s inline environment: block — there are no per-service env_files. Secrets are declared required with ${VAR:?…}, so a missing value fails the run fast:
POSTGRES_PASSWORD,REDIS_PASSWORD— datastore credentialsCLERK_SECRET_KEY/CLERK_PUBLISHABLE_KEY/CLERK_AUTHORIZED_PARTIES/CLERK_WEBHOOK_SECRET— authENCRYPTION_SECRET_KEY— 64-hex field encryption keyGATEWAY_INTERNAL_TOKEN— gateway ↔ backend trustCORS_ORIGINS,MAGIC_CORE_IMAGE_TAG(defaultsmain), and the cliproxy/relay upstream URLs
The deploy/env.*.example files are reference templates for the variable set. For the full list see Configuration.
CI: affected-image builds
.github/workflows/deploy.yml runs all tests first, then uses dorny/paths-filter to decide which images to rebuild:
| Change | Rebuilds |
|---|---|
apps/platform-api/** | platform-api |
apps/agent-service/** | agent-service |
apps/gateway/** | gateway |
apps/relay/** | relay |
apps/keeper/** | keeper |
packages/** · lockfile · workspace | all five app images (they consume @magic/*) |
| docs / compose / deploy scripts | no image rebuild |
Images build as a matrix job, skipping unchanged apps, and are pushed to GHCR.