Skip to content

Deployment

Platos ships as a Docker Compose stack. One docker compose up and you have the backend, dashboard, database, Redis, and a Celery worker + beat scheduler running.

Prerequisites

  • Docker 24+ and Docker Compose v2
  • 4 GB RAM free (Postgres + pgvector + Redis + API + worker + web)
  • Ports 3000 (web), 8000 (API), 5432 (Postgres), 6379 (Redis) free

For local development without containers you can also install uv (Python) and pnpm (Node) - both pinned in the repo root.

The services

From docker-compose.yml (and the table in the root README.md):

ServiceImage / sourcePurpose
platos-dbpgvector/pgvector:pg16Postgres 16 + pgvector for 1024-dim embeddings.
platos-redisredis:7-alpineWarm memory cache, Celery broker, session state.
platos-apiapps/api/DockerfileFastAPI + Pydantic AI agent runtime.
platos-workerapps/api/DockerfileCelery worker - memory extraction, long-running tasks.
platos-beatapps/api/DockerfileCelery beat scheduler.
platos-webapps/web/DockerfileNext.js 15 dashboard.

Every image is pinned - no :latest tags, no surprise updates.

Bootstrapping a fresh checkout

Terminal window
# 1. Clone + bootstrap (installs Python + JS deps, creates .env from template)
git clone https://github.com/tejassudsfp/platos.git
cd platos
make bootstrap
# 2. Generate local secrets into .env (Fernet key + JWT secret)
make gen-keys
# 3. Start everything
make up # full stack
# or
make up-infra # just Postgres + Redis, run API/web locally
# 4. Verify
curl http://localhost:8000/health
open http://localhost:3000

make help lists the full target set.

Required secrets

Two secrets must be set before staging / production will start - the backend hard-fails at import time if either is missing or too weak. This is enforced in apps/api/platos/config.py with an InsecureConfigError to keep a misconfigured prod from silently accepting traffic.

Env varPurposeRequirement
PLATOS_FERNET_KEYEncrypts every API key and OAuth token at rest (PRD §9).32-byte urlsafe base64 - generate with python -m cryptography.fernet.generate_key or make gen-keys.
PLATOS_JWT_SECRETSigns access + refresh tokens for the dashboard API.Minimum 32 characters.

Development mode (PLATOS_ENV=development) skips the gate so make up works from a fresh clone - the make gen-keys target writes real keys either way, so there’s no reason to keep the placeholders around for long.

Database migrations

Every migration is reversible (PRD §9) - downgrade() must restore the previous schema exactly. Run them with Alembic:

Terminal window
# Apply every pending migration
uv run alembic upgrade head
# Roll back the last migration (useful in dev)
uv run alembic downgrade -1

The Compose stack auto-runs alembic upgrade head on platos-api startup so a fresh docker compose up lands with the DB fully migrated. Production deploys should run migrations as a pre-deploy step against the same image.

Scaling notes

  • platos-api is stateless. Scale horizontally behind any L4 load balancer. WebSocket connections from SDKs are sticky to whichever replica accepted them - use a long-lived LB connection with proxy_read_timeout >= 10 minutes so heartbeats don’t kill idle sessions.
  • platos-worker scales independently. Celery handles distribution; add replicas when memory extraction queues back up (Phase C metrics).
  • platos-db is a hard dependency on Postgres. Production deploys typically swap the Compose service for RDS / Cloud SQL / Neon and point PLATOS_DATABASE_URL at it.
  • platos-redis is a hard dependency on Redis. Same pattern - swap for ElastiCache / Upstash in production.

Observability

The API writes structured JSON logs to stdout - your log collector (Datadog, Grafana Loki, Papertrail, etc.) picks them up off the container logs. Metrics and quality-check events land in Postgres and are queryable via the dashboard’s Monitoring tab (Phase H).

HTTPS with Caddy

For production deploys, put a reverse proxy in front of the API and dashboard. Caddy auto-provisions TLS certificates:

# Caddyfile
platform.platos.dev {
reverse_proxy platos-api:8000
}
app.platos.dev {
reverse_proxy platos-web:3000
}

Add a caddy service to your docker-compose.override.yml:

services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
depends_on:
- platos-api
- platos-web
volumes:
caddy_data:

WebSocket connections (/ws/sdk) are proxied automatically. Caddy handles the Upgrade header without extra configuration.

Environment variables reference

Copy .env.example at the repo root to .env and fill in values. Key variables:

VariableRequiredDescription
PLATOS_FERNET_KEYProduction32-byte urlsafe base64 key for encrypting secrets at rest.
PLATOS_JWT_SECRETProductionMin 32 characters. Signs access + refresh tokens.
PLATOS_DATABASE_URLIf not using Compose DBPostgres connection string.
PLATOS_REDIS_URLIf not using Compose RedisRedis connection string.
PLATOS_ENVNodevelopment (default) or production. Production enforces key strength.

Docs site (Vercel)

The marketing site and docs (marketing/) deploy to Vercel as a static Astro build:

Terminal window
cd marketing
pnpm install
pnpm build # outputs to dist/

The Vercel project auto-deploys from the main branch. The build command is cd marketing && pnpm build with output directory marketing/dist. No server-side runtime is needed - the docs are fully static.

Upgrades

  • Pin to a release tag. The Compose file references images by digest in CI; local dev uses :latest for speed.
  • Run migrations first. If a new release includes a migration, run alembic upgrade head before the new API container starts serving traffic. The stateless API can roll through with a brief window of mixed versions; the DB can’t.
  • Roll back via alembic downgrade. Every migration is reversible, and releases publish the revision IDs so you can target a specific version.

Next steps