Skip to content

Deployment

Yigtwxx edited this page Jul 12, 2026 · 1 revision

Deployment

Maestro deploys as a single-host Docker Compose stack behind one Caddy reverse proxy. A 4 GB VM is sufficient (the reference host is an Oracle Cloud Always Free Ampere arm64 instance). Full runbook: docs/DEPLOYMENT.md.

Why not serverless

WebSockets, tasks that run up to 1800s (vs a typical 300s serverless ceiling), and background work that continues after the HTTP response make a long-lived container the right shape. See Realtime-and-WebSockets and Agent-Orchestration.

Production topology (single origin)

Only Caddy publishes ports (80/443). It auto-provisions TLS and routes by path:

  • @api path /api/* /health*backend:8000 (WebSocket upgrades handled automatically; no read timeout on hijacked connections, so 30-minute task streams stay open).
  • @umami path /a/script.js /a/api/sendumami:3000 (only these two analytics paths are public; the dashboard is not exposed).
  • everything else → frontend:3000 (including /docs, which is a marketing page in prod — FastAPI Swagger is disabled when ENVIRONMENT=production).

CORS disappears entirely and NEXT_PUBLIC_* fall to empty defaults, so the image is domain-agnostic and built once.

docker-compose.prod.yml services

Compose project maestro; a shared x-logging anchor caps every service at json-file 10m × 5 (≈550 MB total). Memory limits per service.

Service Image Public ports Role
postgres postgres:16-alpine none Relational DB (password required).
mongo mongo:7 none Logs/sessions/marketplace (root auth).
qdrant qdrant/qdrant:v1.18.2 (pinned) none Vectors (API key; no healthcheck — distroless).
redis redis:7-alpine none Rate-limit + event bus (password, allkeys-lru, no persistence).
ollama ollama/ollama:latest none Embeddings only by default.
ollama-pull ollama/ollama:latest One-shot; pulls nomic-embed-text.
migrate ghcr.io/yigtwxx/maestro-backend One-shot alembic upgrade head; backend waits on service_completed_successfully.
backend ghcr.io/yigtwxx/maestro-backend:${IMAGE_TAG} none FastAPI app.
frontend ghcr.io/yigtwxx/maestro-frontend:${IMAGE_TAG} none Next standalone server.
umami-db-init / umami postgres / umami v3.2 loopback 3001 Optional analytics (profile analytics).
caddy caddy:2-alpine 80/443 Reverse proxy + TLS.

Named volumes: pgdata, mongodata, qdrantdata, ollamadata, caddydata, caddyconfig.

Container images

  • backend/Dockerfile — 2-stage python:3.11-slim-bookworm: builder installs into a venv (all deps ship manylinux wheels, no compiler), runtime copies the venv + app/ + alembic/ + alembic.ini, runs as non-root appuser, healthcheck hits /health, CMD uvicorn ... --workers ${WEB_CONCURRENCY:-1}. Alembic ships in the image because migrate and the purge cron reuse it.
  • frontend/Dockerfile — 2-stage node:20-bookworm-slim (both stages bookworm-slim so the standalone native trace doesn't break on a musl/glibc mix): builder npm ci + npm run build (output: 'standalone'), runner copies .next/standalone + .next/static, non-root node, CMD node server.js. No public/ dir.

Models in production

The hosted ollama serves embeddings only (OLLAMA_CHAT_ENABLED=false), because a chat model needs more RAM. A hosted instance cannot reach a user's local Ollama — all LLM calls are backend-side, so localhost:11434 is the server itself. To offer a free chat tier, raise the ollama memory limit above 2g, pull qwen3.5:9b, and set OLLAMA_CHAT_ENABLED=true. See Configuration.

Scaling

WEB_CONCURRENCY scales Uvicorn workers. Multi-worker requires REDIS_URL — the event bus (maestro:events:{id}), control channel (maestro:ctrl:{id}), and crash reconciliation all coordinate through Redis. See Realtime-and-WebSockets.

CI/CD (.github/workflows/)

Workflow Trigger Does
ci.yml push/PR to main Backend: ruff check, ruff format --check, pytest. Frontend: lint, type-check, build. PR-only Docker build verification of both Dockerfiles.
docker-publish.yml push main, tags v* Builds + pushes images to GHCR. amd64 on main; amd64+arm64 on release tags (Oracle Ampere is arm64).
deploy.yml tags v*, manual dispatch environment: production (reviewer-gated); SSHes to the host and runs compose pull && up -d at /opt/maestro.
codeql.yml push/PR + weekly SAST for Python + JS/TS.
security.yml push/PR Blocking: gitleaks, dependency-review (fail-on high). Informational: semgrep, pip-audit, npm audit, trivy-fs.
pr-lint.yml PR Enforces Conventional-Commit PR titles.
scorecard.yml cron + push OpenSSF Scorecard.
workflow-lint.yml push/PR actionlint + zizmor + merge-conflict scan.

The migrate one-shot runs alembic upgrade head; the backend waits on service_completed_successfully, so a failed migration leaves the old backend up rather than starting a broken one.

First deploy (summary)

  1. Install Docker (Compose v2.17+), point DNS at the host, open 80/443.
  2. Place three files at /opt/maestro: docker-compose.prod.yml, Caddyfile, .env.prod (chmod 600).
  3. Generate secrets, docker compose -f docker-compose.prod.yml up -d.
  4. Seed the marketplace.
  5. Add a cron for account purge: 0 3 * * *python -m app.scripts.purge_deleted_accounts (see Security).

Rollback is IMAGE_TAG=x.y.z ... up -d (does not roll back the DB). Full details, Oracle-specific iptables notes, backups, and monitoring: docs/DEPLOYMENT.md.

Production decisions

  • CODE_EXECUTION_ENABLED=false (enabling requires the Docker socket = host takeover).
  • Mongo root auth + ?authSource=admin mandatory; Qdrant pinned to v1.18.2.
  • /health liveness + /health/ready readiness (503 when degraded); backend + frontend Sentry; X-Request-ID / maestro.access structured logs. No Prometheus/Grafana (RAM) — uptime via external /health checks.

Clone this wiki locally