Skip to content
GitHub

Simplest start: one PostgreSQL

The smallest production layout: one Fleans engine container, the admin UI, optionally the plugin host, and one PostgreSQL server that holds everything — the workflow journal, the read model, BPMN timers and the Orleans membership table. There is no Redis and no Kafka to run, back up or monitor.

Choose it when you are starting out, when the load fits one engine container, or when your team already runs PostgreSQL and nothing else. When you outgrow it, move to the scalable layout: the BPMN files, the REST API and the admin UI stay the same.

Your backend servicesstart, messages, signals (REST)OperatorsbrowserOrleans cluster · Memory streamsfleans · Combined roleone silo: REST API, workflow grains,scripts, read-model projector, timersfleans-pluginsPlugin role · optionalREST Caller,Camunda external tasksfleans-webadmin UIOrleans clientPostgreSQL · one serverfleans: journal + snapshots, read model, membership, reminderscamunda_external_tasks: the Camunda plugin's waiting tasksappend by InstanceId
One engine silo in the Combined role does everything; the admin UI joins the same cluster and the custom-task worker connects over gRPC. Every byte of state lives in one PostgreSQL server.
ComponentImageRole
fleansghcr.io/nightbaker/fleans-apiCombined: REST API, workflow instances, timers, messages and signals, scripts and the read-model projector — the whole engine in one silo.
fleans-webghcr.io/nightbaker/fleans-webAdmin UI, an Orleans client of the cluster.
fleans-plugins (optional)ghcr.io/nightbaker/fleans-pluginsThe ready-made plugins — REST Caller and Camunda 7 external tasks — on a gRPC worker.
postgrespostgres:16 or a managed PostgreSQLEverything below.
DataWhereSetting
Workflow journal, snapshots, read model, definitionsdatabase fleansPersistence__Provider=Postgres, ConnectionStrings__fleans
Orleans cluster membershipsame database (orleansmembershiptable)Fleans__Clustering__Provider=Postgres
BPMN timers (Orleans reminders)same database (orleansreminderstable)Fleans__Reminders__Provider=Postgres
Streams (custom-task dispatch, read-model notifications)in memory of the silosFleans__Streaming__Provider=Memory
Camunda plugin’s waiting tasksdatabase camunda_external_tasks, same serverConnectionStrings__camunda-external-tasks

Fleans creates every table at startup; you only provide an empty database and a user that owns it.

Save this as docker-compose.yml and set POSTGRES_PASSWORD in a .env file next to it:

name: fleans
x-fleans-env: &fleans-env
Persistence__Provider: Postgres
ConnectionStrings__fleans: "Host=postgres;Database=fleans;Username=fleans;Password=${POSTGRES_PASSWORD:-fleans}"
Fleans__Clustering__Provider: Postgres
Fleans__Reminders__Provider: Postgres
Fleans__Streaming__Provider: Memory
Orleans__ServiceId: fleans
Orleans__ClusterId: fleans
x-http-health: &http-health
# The aspnet base image has no curl: a plain HTTP GET /health over bash's /dev/tcp.
test: ["CMD-SHELL", "bash -c 'exec 3<>/dev/tcp/localhost/8080 && printf \"GET /health HTTP/1.0\\r\\nHost: localhost\\r\\n\\r\\n\" >&3 && read r <&3 && [[ \"$$r\" == *200* ]]'"]
interval: 10s
timeout: 5s
retries: 15
start_period: 30s
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: fleans
POSTGRES_USER: fleans
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-fleans}
volumes: [pgdata:/var/lib/postgresql/data]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fleans -d fleans"]
interval: 5s
retries: 20
fleans:
image: ${FLEANS_API_IMAGE:-ghcr.io/nightbaker/fleans-api:0.11.0}
environment:
<<: *fleans-env
Fleans__Role: Combined
ports: ["8080:8080"]
healthcheck: *http-health
depends_on:
postgres: { condition: service_healthy }
restart: unless-stopped
fleans-web:
image: ${FLEANS_WEB_IMAGE:-ghcr.io/nightbaker/fleans-web:0.11.0}
environment: *fleans-env
ports: ["8081:8080"]
depends_on:
fleans: { condition: service_healthy }
restart: unless-stopped
fleans-plugins:
image: ${FLEANS_PLUGINS_IMAGE:-ghcr.io/nightbaker/fleans-plugins:0.11.0}
environment:
Fleans__ExternalTaskWorker__EngineUrl: "http://fleans:8080"
# The Camunda external-task plugin's table, in its own database on the same server.
ConnectionStrings__camunda-external-tasks: "Host=postgres;Database=camunda_external_tasks;Username=fleans;Password=${POSTGRES_PASSWORD:-fleans}"
ports: ["8082:8080"]
depends_on:
fleans: { condition: service_healthy }
restart: unless-stopped
volumes:
pgdata:
Terminal window
echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)" > .env
docker compose up -d --wait

The REST API is on http://localhost:8080 (Swagger at /swagger), the admin UI on http://localhost:8081, and the plugin worker serves the Camunda /engine-rest API on http://localhost:8082. Drop the fleans-plugins service if you don’t use the plugins.

To use a PostgreSQL server you already have, delete the postgres service and point ConnectionStrings__fleans (and the Camunda plugin’s connection string) at it.

The same layout with the Helm chart:

values-single-postgres.yaml
# One engine silo, the admin UI and MCP, the custom-task worker — and one PostgreSQL. No Redis.
persistence:
provider: Postgres
postgres:
enabled: true # or false + ConnectionStrings__fleans in extraEnv for your own server
clustering:
provider: Postgres
reminders:
provider: Postgres
streaming:
provider: Memory
redis:
enabled: false
core:
replicas: 1 # Memory streams: keep one engine silo
plugins:
enabled: true # optional: REST Caller + Camunda external tasks
Terminal window
gh release download v0.11.0 --repo nightBaker/fleans -p 'fleans-0.11.0.tgz'
helm install fleans ./fleans-0.11.0.tgz -f values-single-postgres.yaml

The chart’s engine silo runs in the Core role; with no Worker silos it runs scripts and the read-model projector itself, exactly like Combined.

  • One engine silo. Memory streams live inside the silos, so keep a single fleans (core.replicas: 1). A restart is a short outage of the REST API; nothing durable is lost — instances, timers and messages are in PostgreSQL, and timers that came due during the restart fire when the silo is back.
  • In-flight stream messages die with the process. If the engine crashes while a custom task is being handed to its worker, that task stays active and needs a manual retry; read-model notifications repair themselves on the instance’s next activation. Start the worker before you run processes that use its tasks.
  • One database to size. Every step is a write to the same PostgreSQL. When its CPU or I/O becomes the limit, that is the signal to move on.

Move to the scalable layout when you need more than one engine silo (availability or CPU), when PostgreSQL write load becomes the bottleneck, or when you want the admin UI’s queries off the database that runs workflows. The move is configuration only, but until 1.0 there is no data migration between layouts or releases: start it on fresh databases.