Skip to main content

Run in production

This page covers running SignalPilot yourself in cloud mode: a multi-tenant deployment with Clerk sign-in, behind TLS, with hosted sandboxes for notebooks and chat. If you want the single-machine stack for one team, Self-host with Docker Compose is enough. If you would rather not run anything, use SignalPilot Cloud.

What cloud mode changes

SP_DEPLOYMENT_MODE=cloud switches on three things:

  • Multi-tenant auth. Users sign in through Clerk. API keys are scoped to an organization, and plan quotas are enforced.
  • Hosted sandboxes. Notebooks and chat sessions run on the hosted sandbox backend instead of the shared local notebook container. Each session gets its own isolated microVM.
  • Hardening checks. At boot the gateway verifies its configuration and refuses to start if a rule is broken. The error lists variable names only, never values.

The rules are exactly these:

RuleWhy
SP_NOTEBOOK_DIRECT_URL must be emptyThe shared notebook container is a local-only convenience.
SP_DISABLE_SANDBOX must not be true, 1, or yesSandboxing cannot be switched off for tenants.
SP_ALLOWED_ORIGINS must be set, with no wildcards, and every entry https:// or loopbackBrowsers must only reach the gateway from your own app.

A broken rule produces Cloud mode hardening violations: [...]. Refusing to boot. in the logs.

Prerequisites

  • Postgres for the gateway's own state.
  • S3-compatible object storage with buckets for workspaces, chat artifacts, and eval evidence. MinIO works.
  • A sandbox backend. Cloud mode uses the hosted sandbox backend. Its credentials are provisioned outside this configuration surface; ask us before you plan a cloud-mode deployment.
  • A Clerk application for sign-in.
  • A TLS reverse proxy such as Caddy, nginx, or your load balancer.
  • A container registry you control, for the gateway image and the digest-pinned sandbox image.

Required environment

Everything from the configuration reference still applies. These are the values cloud mode cannot run without.

VariableSet it to
SP_DEPLOYMENT_MODEcloud
DATABASE_URLpostgresql+asyncpg://user:pass@host:5432/signalpilot
SP_ENCRYPTION_KEYA Fernet key you generated. Never the example value from docker-compose.yml.
SP_SESSION_JWT_SECRETA long random string.
SP_ALLOWED_ORIGINSYour app origin, for example https://app.your-domain.example
SP_PUBLIC_GATEWAY_URLThe HTTPS URL sandboxes use to reach the gateway, for example https://gateway.your-domain.example
CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEYFrom the Clerk dashboard, under API Keys.
SP_EXPECTED_AZPYour app origin. Recommended; binds Clerk session tokens to your front end.
SP_ADMIN_USER_IDSComma-separated Clerk user ids for your platform admins.
SP_NOTEBOOK_EXECUTION_BACKENDvercel (the hosted sandbox backend)
SP_NOTEBOOK_VERCEL_IMAGEyour-registry/notebook@sha256:<64 hex>
SP_WORKSPACE_S3_BUCKET and its _ENDPOINT, _REGION, _ACCESS_KEY, _SECRET_KEYYour workspace bucket.
SP_CHAT_OBJECTS_BUCKET and its _S3_ENDPOINT, _S3_REGION, _S3_ACCESS_KEY, _S3_SECRET_KEYYour chat artifacts bucket.

Leave CLERK_JWT_AUDIENCE unset unless you configured a Clerk JWT template that emits an aud claim. Clerk's default session tokens carry azp, which is why SP_EXPECTED_AZP is the check to use.

Pin the sandbox image by digest

A floating tag such as :latest could be re-pushed under you, so cloud mode refuses it. Resolve the digest after you push:

resolve the image digest
$ crane digest your-registry/notebook:VERSION
# or
$ docker buildx imagetools inspect your-registry/notebook:VERSION

Generate the secrets

generate secrets
# SP_ENCRYPTION_KEY
$ python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
# SP_SESSION_JWT_SECRET
$ openssl rand -hex 32

Run the gateway and worker

Build the gateway image from the signalpilot/gateway/ directory and push it to your registry:

build and push the gateway image
$ docker build -f Dockerfile.gateway -t your-registry/gateway:VERSION signalpilot/gateway/
$ docker push your-registry/gateway:VERSION

The gateway and the chat worker share one image. Run both, from the same environment file, and always upgrade them together:

docker-compose.prod.yml
services:
gateway:
image: your-registry/gateway:VERSION
env_file: gateway.env
ports:
- "127.0.0.1:3300:3300"
restart: unless-stopped
gateway-chat-worker:
image: your-registry/gateway:VERSION
env_file: gateway.env
command: signalpilot-chat-worker
restart: unless-stopped
start
$ docker compose -f docker-compose.prod.yml up -d

Bind the gateway to loopback and terminate TLS in front of it. A minimal Caddy configuration:

caddy reverse proxy
gateway.your-domain.example {
reverse_proxy 127.0.0.1:3300
}

SP_ALLOWED_ORIGINS and SP_PUBLIC_GATEWAY_URL must use the public HTTPS origin, not the container address.

Secrets and key rotation

  • Never bake secrets into images. Inject SP_ENCRYPTION_KEY, SP_SESSION_JWT_SECRET, CLERK_SECRET_KEY, DATABASE_URL, and the object storage credentials at runtime from your platform's secret manager.
  • The gateway never logs secret values. Hardening violations name variables only. Keep it that way; do not echo the environment into logs.
  • Rotate SP_ENCRYPTION_KEY without downtime. Set the new key in SP_ENCRYPTION_KEY and move the old one into SP_ENCRYPTION_KEY_OLD (comma-separate several if needed). Old keys decrypt only; every credential is re-encrypted under the new key the next time it is read. Once every connection has been used at least once, drop the old key.

Health checks

  • GET /health returns {"status":"healthy"} when the process is up. It does not touch the database. Use it as a liveness probe.
  • For readiness, call an authenticated route that exercises the database, such as GET /api/connections with a valid API key.
smoke test
$ curl -sf https://gateway.your-domain.example/health
$ curl -sf https://gateway.your-domain.example/api/connections -H "X-API-Key: sp_..."

Then point an MCP client at https://gateway.your-domain.example/mcp, run connection_health, and confirm every connection reports reachable. If the gateway fails to start, search the logs for Cloud mode hardening violations.

Upgrading

  1. Back up Postgres and confirm your encryption key is stored safely.
  2. Pull or build the new gateway image and, if the release notes say so, push a new sandbox image and update SP_NOTEBOOK_VERCEL_IMAGE with its digest.
  3. Recreate the gateway and the worker from the same image. A worker on an older image than the gateway will pick up runs it cannot handle.
  4. Watch the first boot. Schema migrations run automatically under a database lock, so multiple replicas starting at once is safe. To run them by hand instead, use uv run alembic upgrade head from signalpilot/gateway/.
  5. Existing warm sandboxes keep their old image until their idle snapshot. Test new behaviour in a fresh session.

For backups, retention, and the background loops to watch, read Operations.