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

ImagePurpose
synapseorchai/synapse-aiMain instance — combined UI + API server. Hosts the UI and Settings → Scale, serves the APIs, and enqueues V2 jobs.
synapseorchai/synapse-ai-workerExecution tier — no UI. Scale this horizontally. Pulls jobs from Redis and executes workflow runs and agent chat turns.
synapseorchai/synapse-ai-api-serverAPI 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 yes

The --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:latest

Verify 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=3
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:latest

Scale 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:latest

All 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 .env

Leave 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/0

Compose 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 -d

This starts five services:

ServiceContainerPurposePort
backendsynapse-backendAPI server8765, on 127.0.0.1 only
frontendsynapse-frontendWeb UI, and the public path to /api/v1 and /api/v23000
postgressynapse-postgresThe store—
redissynapse-redisJob queue, with AOF persistencenone published
workerone per replica, named by ComposeARQ workernone 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=3

Workers on other machines are started with Option 1, against the same Redis, database and blob storage.

Stop all services

docker compose --profile scale down

docker 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
  • kubectl configured and targeting the correct cluster
  • Redis and Postgres reachable from inside the cluster
  • A SynapseOrch main instance (the synapseorchai/synapse-ai image) 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.yaml

This creates:

ManifestWhat it deploys
api-deployment.yamlsynapse-api Deployment (synapse-ai-api-server, 3 replicas) + ClusterIP Service on port 8765 + HPA (2–20 replicas at 70% CPU)
worker-deployment.yamlsynapse-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 pods

Check worker logs to confirm successful registration:

kubectl logs -l app=synapse-worker --tail=50

Check API server logs:

kubectl logs -l app=synapse-api --tail=50

Expose 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: 8765

The 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.0

Change the number of workers:

kubectl scale deployment/synapse-worker --replicas=5

Watch a rolling update in progress:

kubectl rollout status deployment/synapse-worker

Production checklist

Work through this list before going live with a scale deployment.

Infrastructure

  1. Redis persistence — enable AOF (appendonly yes). Without persistence, a Redis restart drops all queued jobs.
  2. No eviction of the queue — give Redis enough memory, and don't use an eviction policy that can remove queued jobs.
  3. 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.
  4. Postgres backups — Postgres is the whole store: definitions, settings, run history. orchestration_runs and chat_sessions grow over time and are not cleaned up automatically.
  5. 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=1 on every process.

Workers

  1. At least 2 worker replicas — prevents a complete execution outage if one worker crashes.
  2. 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.
  3. 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

  1. One blob store — configure the same S3 bucket on the main instance, API replicas and workers, or mount the same SYNAPSE_BLOB_DIR on all of them. Without that, a vault file or run log written by one process is invisible to the next.

Security

  1. 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 to 127.0.0.1.
  2. 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.
  3. API keys — V2 calls authenticate with API keys from Settings → API keys. Give each client its own key, so one can be revoked alone.
  4. imagePullPolicy: Always — set on the API and worker manifests, so a restart pulls the current latest image. 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:latest

Jobs 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 worker

Compose 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-worker

Watch the rolling update progress:

kubectl rollout status deployment/synapse-worker

Kubernetes 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.

On this page