Deployment
SynapseOrch publishes three Docker images to Docker Hub. Choose the deployment pattern that fits your infrastructure — from a single docker run command to Kubernetes.
Docker images
| Image | Purpose |
|---|---|
synapseorchai/synapse-ai | Main instance — combined UI + API server. Hosts the UI and Settings → Scale, serves the APIs, and enqueues V2 jobs. |
synapseorchai/synapse-ai-worker | Execution tier — no UI. Scale this horizontally. Pulls jobs from Redis and executes workflow runs and agent chat turns. |
synapseorchai/synapse-ai-api-server | API tier replica — the API without the UI. Use behind a load balancer when the API tier itself becomes the bottleneck. |
All three images are published together on every release, tagged with the version and latest — for example synapseorchai/synapse-ai-worker:2.0.0.
Every process in a deployment — main instance, API replicas, workers — must point at the same Redis, the same Postgres and the same blob storage. See Scale Mode → What you need.
Option 1 — Docker Run
The fastest path to adding workers to an existing SynapseOrch install. Run this on any machine that can reach your Redis and Postgres instances and your shared blob storage.
Prerequisites
- Docker installed
- A running Redis instance (see below)
- A running Postgres instance — the one your main instance uses as
SYNAPSE_DB_URL - The same blob storage as the main instance: the S3 bucket it uses, or its blob directory (
SYNAPSE_BLOB_DIR) available on this machine
Start Redis (if you don't have one)
docker run -d \
--name synapse-redis \
-p 6379:6379 \
-v synapse-redis-data:/data \
redis:7-alpine \
redis-server --appendonly yesThe --appendonly yes flag enables AOF persistence — your queue survives a Redis restart.
Start the worker
docker run -d \
--name synapse-worker-1 \
-e REDIS_URL="redis://your-redis-host:6379/0" \
-e SYNAPSE_DB_URL="postgresql://user:pass@your-pg-host:5432/synapse" \
-e WORKER_CONCURRENCY=10 \
-e WORKER_JOB_TIMEOUT=3600 \
-e WORKER_MAX_RETRIES=3 \
-v /shared/synapse-blobs:/blobs \
-p 9000:9000 \
--restart unless-stopped \
synapseorchai/synapse-ai-worker:latestVerify the worker is healthy
curl http://localhost:9000/health{
"status": "ok",
"worker_id": "worker-4f1c2a9b7e30-1a2b3c4d",
"address": "http://4f1c2a9b7e30:9000",
"active_jobs": 0,
"max_jobs": 10,
"uptime_seconds": 18,
"pg_connected": true,
"redis_connected": true
}The worker registers itself as soon as it starts. Click refresh on Workers in Settings → Scale to see it. The address is the container's hostname, which the main instance usually cannot reach — the health check in the Workers list only works where it can.
Use an env file
For cleaner deployments, put all configuration in a .env file:
# .env
REDIS_URL=redis://your-redis:6379/0
SYNAPSE_DB_URL=postgresql://user:pass@your-pg:5432/synapse
WORKER_CONCURRENCY=10
WORKER_JOB_TIMEOUT=3600
WORKER_MAX_RETRIES=3docker run -d \
--name synapse-worker-1 \
--env-file .env \
-v /shared/synapse-blobs:/blobs \
-p 9000:9000 \
--restart unless-stopped \
synapseorchai/synapse-ai-worker:latestScale to multiple workers
Run the same command on more machines. On one machine, give each worker its own container name and host port:
docker run -d --name synapse-worker-2 --env-file .env \
-v /shared/synapse-blobs:/blobs \
-p 9001:9000 --restart unless-stopped \
synapseorchai/synapse-ai-worker:latestAll workers share the same Redis queue. Jobs are distributed automatically — no additional configuration is needed.
Option 2 — Docker Compose
The docker-compose.yml in the repository runs SynapseOrch with its own Postgres. Its scale profile adds Redis and one worker, sharing the database and a blob volume with the backend.
Prerequisites
- Docker and Docker Compose v2+
- The SynapseOrch repository cloned locally
Configure environment
Copy the environment template:
cp .env.docker .envLeave SYNAPSE_INTERNAL_TOKEN blank — the containers generate one on first start and share it through a volume. Then set the Redis URL, which is what puts the backend in scale mode:
REDIS_URL=redis://synapse-redis:6379/0Compose gives the backend and the worker the same Postgres, and the scale layer uses it, so there is nothing else to set. SCALE_POSTGRES_URL is only for pointing the scale layer at a different URL for that database.
Start with the scale profile
docker compose --profile scale up -dThis starts five services:
| Service | Container | Purpose | Port |
|---|---|---|---|
backend | synapse-backend | API server | 8765, on 127.0.0.1 only |
frontend | synapse-frontend | Web UI, and the public path to /api/v1 and /api/v2 | 3000 |
postgres | synapse-postgres | The store | — |
redis | synapse-redis | Job queue, with AOF persistence | none published |
worker | one per replica, named by Compose | ARQ worker | none published; health on 9000 inside the network |
Compose starts Redis with a 2 GB memory limit and the noeviction policy, so a full Redis refuses new writes rather than dropping queued jobs. Raise the limit if your queue can grow large.
The worker service has no fixed container name and publishes no host port, so you can run several:
docker compose --profile scale up -d --scale worker=3Workers on other machines are started with Option 1, against the same Redis, database and blob storage.
Stop all services
docker compose --profile scale downdocker compose --profile scale down -v also deletes every volume — the database, the vault and run logs, the generated internal token, and the Redis data. Use it only to start again from nothing.
Option 3 — Kubernetes
infra/k8s/ in the repository holds two manifests, one for an API Deployment and one for a worker Deployment. They don't include the main instance with the UI, Redis, Postgres, or blob storage — you provide those.
Prerequisites
- A Kubernetes cluster
kubectlconfigured and targeting the correct cluster- Redis and Postgres reachable from inside the cluster
- A SynapseOrch main instance (the
synapseorchai/synapse-aiimage) using the same Redis and Postgres — for the UI, definitions and API keys - Blob storage the pods share: an S3 bucket (or MinIO or R2), with the
S3_*variables added to both Deployments. The manifests mount no volume, so without a bucket each pod writes to its own disk and cannot read what another wrote
Create secrets
kubectl create secret generic synapse-secrets \
--from-literal=redis-url="redis://your-redis:6379/0" \
--from-literal=postgres-url="postgresql+asyncpg://user:pass@your-pg:5432/synapse"Both Deployments pass postgres-url as SYNAPSE_DB_URL as well as SCALE_POSTGRES_URL. Either spelling of the URL works: postgresql:// and postgres:// are converted to postgresql+asyncpg:// for you.
Apply the manifests
kubectl apply -f infra/k8s/api-deployment.yaml -f infra/k8s/worker-deployment.yamlThis creates:
| Manifest | What it deploys |
|---|---|
api-deployment.yaml | synapse-api Deployment (synapse-ai-api-server, 3 replicas) + ClusterIP Service on port 8765 + HPA (2–20 replicas at 70% CPU) |
worker-deployment.yaml | synapse-worker Deployment (2 replicas, WORKER_CONCURRENCY=20, 1Gi memory request, 4Gi limit) + Service on port 9000 |
There are no other manifests: the KEDA ScaledObject and the PgBouncer Deployment that used to sit beside these did not work as written and have been removed. Scale the workers with kubectl scale, and run your own PgBouncer if you want one.
Verify the deployment
kubectl get deployments
kubectl get podsCheck worker logs to confirm successful registration:
kubectl logs -l app=synapse-worker --tail=50Check API server logs:
kubectl logs -l app=synapse-api --tail=50Expose the API with an Ingress
The synapse-api Service is a ClusterIP on port 8765. Add an Ingress to expose it externally:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: synapse-ingress
annotations:
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
rules:
- host: api.yourdomain.com
http:
paths:
- path: /api/v2
pathType: Prefix
backend:
service:
name: synapse-api
port:
number: 8765The proxy-read-timeout and proxy-send-timeout annotations must be at least 3600 seconds. SSE connections for long-running workflows stay open for minutes or hours — the default nginx timeout of 60 seconds closes them early.
Expose /api/v2 (and /api/v1 if you use it), not the whole backend: the rest of /api is the UI's own interface.
Customising the manifests
Pin workers to a specific image version:
kubectl set image deployment/synapse-worker worker=synapseorchai/synapse-ai-worker:2.0.0Change the number of workers:
kubectl scale deployment/synapse-worker --replicas=5Watch a rolling update in progress:
kubectl rollout status deployment/synapse-workerProduction checklist
Work through this list before going live with a scale deployment.
Infrastructure
- Redis persistence — enable AOF (
appendonly yes). Without persistence, a Redis restart drops all queued jobs. - No eviction of the queue — give Redis enough memory, and don't use an eviction policy that can remove queued jobs.
- Redis high availability — use a managed Redis with failover, Redis Sentinel or Redis Cluster. A single Redis node is a single point of failure for the job queue.
- Postgres backups — Postgres is the whole store: definitions, settings, run history.
orchestration_runsandchat_sessionsgrow over time and are not cleaned up automatically. - Postgres connections — each process keeps a pool of up to 30 connections (10 plus 20 overflow), and the tool processes it starts open their own. With many workers, put PgBouncer in front of Postgres and set
PGBOUNCER_MODE=1on every process.
Workers
- At least 2 worker replicas — prevents a complete execution outage if one worker crashes.
- Sufficient memory — workers run model calls, tool processes and MCP servers for every job at once. The worker manifest requests 1Gi and limits to 4Gi; raise it if you see OOM kills.
- Stopping workers re-runs their jobs — a worker cancels its running jobs when it receives
SIGTERM, and they run again from the start elsewhere, repeating their model calls and tool side effects. Stop or roll workers when they are idle where you can.
Shared storage
- One blob store — configure the same S3 bucket on the main instance, API replicas and workers, or mount the same
SYNAPSE_BLOB_DIRon all of them. Without that, a vault file or run log written by one process is invisible to the next.
Security
- Expose only the APIs — publish the UI and
/api/v1//api/v2, never the backend's port as a whole. Compose already binds the backend to127.0.0.1. - Network policy — restrict worker pods so they can only reach Redis, Postgres, storage and external model endpoints. Workers accept inbound connections only on port 9000.
- API keys — V2 calls authenticate with API keys from Settings → API keys. Give each client its own key, so one can be revoked alone.
imagePullPolicy: Always— set on the API and worker manifests, so a restart pulls the currentlatestimage. Pin a version tag for predictable rollouts.
Upgrading workers
Docker Run
# Pull the new image
docker pull synapseorchai/synapse-ai-worker:latest
# Stop and remove the old container
docker stop synapse-worker-1 && docker rm synapse-worker-1
# Start a new container with the same configuration
docker run -d --name synapse-worker-1 --env-file .env \
-v /shared/synapse-blobs:/blobs \
-p 9000:9000 --restart unless-stopped \
synapseorchai/synapse-ai-worker:latestJobs that were running when the old container stopped go back on the queue and run again from the start on the next worker.
Docker Compose
docker compose --profile scale pull worker
docker compose --profile scale up -d --scale worker=1 workerCompose stops the old worker containers and starts new ones. Their running jobs are re-run from the start. Pass the number of workers you run to --scale, or Compose brings the service back at one.
Kubernetes
# Update to a specific version tag
kubectl set image deployment/synapse-worker worker=synapseorchai/synapse-ai-worker:2.0.0
# Or, to pull the latest tag again (the manifests set imagePullPolicy: Always)
kubectl rollout restart deployment/synapse-workerWatch the rolling update progress:
kubectl rollout status deployment/synapse-workerKubernetes starts new pods and waits for their readiness probe before stopping old ones. Jobs running on the pods it stops are cancelled and run again from the start on the new pods.