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.
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.What runs where
Section titled “What runs where”| Component | Image | Role |
|---|---|---|
fleans | ghcr.io/nightbaker/fleans-api | Combined: REST API, workflow instances, timers, messages and signals, scripts and the read-model projector — the whole engine in one silo. |
fleans-web | ghcr.io/nightbaker/fleans-web | Admin UI, an Orleans client of the cluster. |
fleans-plugins (optional) | ghcr.io/nightbaker/fleans-plugins | The ready-made plugins — REST Caller and Camunda 7 external tasks — on a gRPC worker. |
postgres | postgres:16 or a managed PostgreSQL | Everything below. |
| Data | Where | Setting |
|---|---|---|
| Workflow journal, snapshots, read model, definitions | database fleans | Persistence__Provider=Postgres, ConnectionStrings__fleans |
| Orleans cluster membership | same 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 silos | Fleans__Streaming__Provider=Memory |
| Camunda plugin’s waiting tasks | database camunda_external_tasks, same server | ConnectionStrings__camunda-external-tasks |
Fleans creates every table at startup; you only provide an empty database and a user that owns it.
Run it with Docker Compose
Section titled “Run it with Docker Compose”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:echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)" > .envdocker compose up -d --waitThe 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.
Run it on Kubernetes
Section titled “Run it on Kubernetes”The same layout with the Helm chart:
# One engine silo, the admin UI and MCP, the custom-task worker — and one PostgreSQL. No Redis.persistence: provider: Postgrespostgres: enabled: true # or false + ConnectionStrings__fleans in extraEnv for your own serverclustering: provider: Postgresreminders: provider: Postgresstreaming: provider: Memoryredis: enabled: falsecore: replicas: 1 # Memory streams: keep one engine siloplugins: enabled: true # optional: REST Caller + Camunda external tasksgh 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.yamlThe 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.
Limits of this layout
Section titled “Limits of this layout”- 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.
When to move on
Section titled “When 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.