Skip to main content

Deployment

A deployment is the frontend, the backend, and one relational database; Run with Docker Compose is the shortest path to all three. What follows is what changes once the stack sits behind a proxy and holds data worth keeping.

The browser only ever talks to the frontend. Its server-side proxy forwards /api/* to the backend, which keeps the auth cookies first-party — so the backend needs no public route of its own. Every variable named here is described under Configuration reference.

Reverse proxy and load balancer​

Sticky sessions / session affinity are not required. See Horizontal scaling — a PostgreSQL advisory lock, not routing affinity, is what keeps one conversation pinned to one driver at a time, and only for the duration of a single SSE stream.

The SSE agent routes (POST /api/v1/workflow-executions/{id}/agent and POST /api/v1/workflows/{id}/agent) need the following at the proxy layer:

SettingRequired valueWhy
Response bufferingOff for those two pathsThe app sends X-Accel-Buffering: no, which nginx honors per-response even with buffering on globally. Every other proxy needs it disabled in its own config
CompressionNever applied to text/event-streamThe app never compresses a response, so any compression the client sees comes from this layer
Read / idle timeoutWell above the longest agent run you expectAn agent run has no server-side time limit, so a proxy read timeout is the only thing that can end a stream — and a low one silently truncates a legitimate long response. Size it against your longest run, not a typical request

A rolling deploy forcibly ends any SSE stream still open 30 seconds after the container receives SIGTERM (--timeout-graceful-shutdown 30, set in the backend Dockerfile's CMD). That is safe: the advisory lock releases with the connection and the client resumes with a fresh request, exactly as an abandoned stream already behaves.

Behind HTTPS​

Three settings default to values that only suit local HTTP development. Change all three before a deployment faces real users:

VariableSet it toLeft at the default
SESSION_COOKIE_SECUREtrueSession and CSRF cookies go out without Secure, so a browser will also send them over plain HTTP
CORS_ORIGINSThe browser-facing origin, e.g. https://a2flow.example.comOnly http://localhost:3000 is allowed to call the API
APP_BASE_URLThe same originNotification email is sent without the deep links back into the app — see Notification email

Process layout​

Alongside the API, a deployment runs the MCP proxy: the process that actually starts or connects to the MCP servers a tenant has registered. Keeping it separate is the point — it is where third-party code runs, so it holds no database access, no secret encryption key and no LLM API key. compose.yml runs it as the mcp-proxy container, on a network it shares only with the API, so it cannot reach the database at all; a deployment that assembles its own containers should keep the proxy off the database's network the same way, and configure it under MCP proxy.

Two things to get right:

  • The API has to start first. It issues the material both ends identify each other with, so the proxy will not accept anything until that has been written. Gate the proxy on the API's health check, as compose.yml does.
  • They need a shared directory. MCP_PROXY_TLS_DIR has to be the same storage on both sides — writable by the API, read-only for the proxy. Nothing durable is kept there: it is rewritten whenever it is missing or close to expiring.
  • The proxy has to be able to build sandboxes. It starts every server in a sandbox of its own, and checks at startup that it can — if it cannot, it stops with an error rather than running servers unsandboxed. Inside a container this takes three security options, which compose.yml gives the mcp-proxy service and only that service: seccomp=unconfined, apparmor=unconfined, and systempaths=unconfined. On an Ubuntu 24.04 or later host, unprivileged user namespaces must also be allowed: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0.

Skipping the proxy is possible — leave MCP_PROXY_URL unset and the API reaches MCP servers itself, which is what a local development run does. It means a registered server runs beside the deployment's credentials, so it is not a choice to make for an environment where anyone can register one.

The backend also ships as two entry points against the same database. Which one you run decides where notification email is delivered from:

LayoutHowUse it when
API onlyThe default — EMAIL_WORKER_IN_PROCESS is true, so uvicorn main:app also drains the email queueA single-process deployment, or one small enough that a stalled relay on the request path is acceptable
API + workerRun python -m worker alongside the API and set EMAIL_WORKER_IN_PROCESS=false on itAnything where a hung relay must not cost anything that serves requests. This is what compose.yml does

Two things about the worker process:

  • It does not migrate or seed. The API's startup owns both, so the worker has to start after the API is up — compose.yml gates it on the API's health check.
  • More than one replica buys failover, not throughput. They contend on the email-queue advisory lock and all but one sit idle by design, which is what keeps the send rate exact.

Health checks​

Point your orchestrator or load balancer at GET /api/v1/health. See Health checks.

State that has to persist​

Three things outlive a container and have to sit on durable storage for a deployment to come back up intact:

WhatWhere it livesLosing it means
The databaseThe database named by DB_URLEverything — every record lives there. See Database
The agent skill storeSKILLS_DIRExecutions cannot load the skill revision they are pinned to (HTTP 409 SKILL_NOT_READY). It is durable state, not a cache — see Agent skill store
The secret encryption keySECRET_ENCRYPTION_KEY, or the file at SECRET_KEY_FILEEvery stored local secret becomes undecryptable, and the certificate authority can no longer issue or verify the certificates every MCP tool call needs. See Secret management

How to take and restore each of them is in Backup and restore.