Skip to main content
The reference Compose stack is built to start on the first try, not to be exposed. This page lists what is wrong with it for production and what to do instead.

What the reference stack is not

Every item below is in docker-compose.yaml or apps/api/app/main.py today:

Build real images

Drop the bind-mounts and the reload flag. Build tagged images, push them to a registry, and deploy those:
Run the API without --reload:

Reverse proxy and TLS

Put a proxy in front of both the API and the runtime. The runtime carries WebSocket audio, so the upgrade headers are mandatory:
Then set VOICE_SERVER_BASE_URL=https://voice.example.com before creating telephony agents — see Public voice URLs.

Tighten CORS

allow_origins=["*"] with credentials is the reference default. Restrict it to the origins that actually call the API, in apps/api/app/main.py, and rebuild.

Externalise the stores

The in-stack postgres, redis, and minio are conveniences. In production prefer managed or dedicated instances with their own backups, monitoring, and upgrade path:

Scaling

Never run more than one campaign orchestrator. Its scheduling state (_batch_in_progress, _processing_locks, _last_activity) is in-memory, and it uses Redis pub/sub, which fans out to every subscriber. Two replicas would both schedule the next batch — dialling at twice the configured rate — and the completion sweep could mark a live campaign completed. Compose pins it implicitly via container_name.

Sizing

Start here and measure: With cloud providers the runtime is mostly waiting on network, so it is rarely CPU-bound. Self-hosted models change the picture entirely — see Running on GPUs.

Secrets

Keep them out of .env on disk. Use your platform’s secret manager and inject at runtime. Every variable the stack reads, with defaults and which service consumes it, is in Environment variables.

Backups

Back up all three durable stores together — a database restored without its recordings is inconsistent:
Redis is ephemeral. Store PROVIDER_AUTH_ENCRYPTION_KEY with the backup — the credentials are useless without it. See Daily operations.

Upgrades

  1. Back up all three stores.
  2. Pull and build the new images.
  3. Restart the API first — initialize_database() reconciles collections and indexes idempotently on startup.
  4. Restart workers and the orchestrator.
  5. Drain and restart runtimes last, so live calls finish.
There is no migration tool; schema is enforced by Pydantic at the edge.

Checklist

  • Images built and tagged; no bind-mounts, no --reload on api
  • TLS on both the API and the runtime, with WebSocket upgrade
  • VOICE_SERVER_BASE_URL set to the public host before creating agents
  • CORS restricted
  • All default passwords changed
  • MinIO console not publicly reachable
  • SECRET_KEY identical across API replicas
  • PROVIDER_AUTH_ENCRYPTION_KEY backed up
  • Exactly one campaign orchestrator
  • Runtime behind session affinity
  • Backups scheduled and a restore tested