Production Setup (Admin + Workers)
For production, Fluxify splits into two roles so you can scale request handling without touching the control plane:
| Role | Image | Responsibility |
|---|---|---|
| Admin | fluxify-admin | Control plane — dashboard, admin API, AI gateway. Owns the database and prepares your routes for the workers. Run one. |
| Orchestrator | fluxify-orchestrator | Starts and stops the worker containers for you. Holds the Docker socket; runs none of your code. Run one (a second one stands by). |
| Worker | fluxify-worker | Serves your published API. Holds no database connection. Run many — the orchestrator creates them. |
All three are published to the GitHub Container Registry under ghcr.io/fluxify-rest/.
An edge proxy (Traefik) sits in front and sends admin traffic to the admin container and everything else to the workers.
TIP
This guide runs the workers on one Docker host. To run them on a Kubernetes cluster instead, see Install on Kubernetes.
Which tag to pull
Every release tags all three images together, so admin, orchestrator and worker always come from the same build. Never mix versions between them.
| Tag | What it is |
|---|---|
v0.0.3-alpha-pre-plugin | One exact release. Pin this for anything you care about. |
alpha | Moves to the newest pre-release. Convenient while Fluxify is pre-1.0. |
latest | The newest stable release. Does not exist yet — the first 1.0 release creates it. |
WARNING
Fluxify is pre-1.0, so every release is a pre-release and latest is not published. Use alpha, or a pinned version tag.
TIP
Just prototyping or testing? The all-in-one Kit image is simpler. Anything in production belongs here, even on a single machine.
Why Traefik here?
The production stack runs multiple worker containers and load-balances across them. Traefik discovers each worker automatically from its Docker labels and spreads traffic across every replica with no manual list to maintain — add or remove workers and routing updates itself. The Kit image uses a simpler built-in proxy because it only ever has one of each service.
Who starts the workers
You don't. There is no worker service in the compose file — the orchestrator creates worker containers, and compose runs only the infrastructure around them.
The reason is that hand-writing a worker service is easy to get wrong in a way nothing tells you about: its settings are a project id, a mode and a list of group ids, and a misplaced one produces a worker that starts happily and serves the wrong things. So you describe what you want instead — "this project wants a workflow node for these groups, two of them" — and the orchestrator creates the containers, labels them for Traefik, and removes them when you stop asking for them.
That request is called a claim. A fresh stack seeds one automatically: a single node serving every project and doing both jobs, which is exactly what the old worker service was. Set ORCHESTRATOR_SEED_DEFAULT_CLAIM=false in your .env to start with no workers at all.
Ownership is split cleanly, because two things managing the same container will fight over it:
- compose owns nats, postgres, valkey, traefik, admin, orchestrator, and the one development worker
- the orchestrator owns every production worker
WARNING
docker compose ... --remove-orphans deletes the orchestrator's workers. Compose has no idea they were never its containers. They come back on the next reconcile pass (about five seconds) as long as the orchestrator is running.
Two other things worth knowing:
- A license that lapses does not take your traffic down. Running workers keep running; only a restart is refused. Fix the licence and nothing had to fail in the meantime.
- Killing the orchestrator does not stop your API. Workers are not its children — they are separate containers with Docker's own restart policy. You can restart or upgrade the control plane while traffic keeps being served.
For how any of that actually works — the leader lease, the reconcile loop, what forces a container to be replaced — see The Orchestrator.
Architecture
Only the admin container connects to PostgreSQL. Workers receive your routes ready to run over NATS — see Request Lifecycle.
Workers only receive traffic once they report ready. Traefik health-checks each replica and holds traffic back until its routes have loaded.
Which projects a worker serves
A fresh stack starts one worker that serves every project. That is the default claim, and its worker runs with WORKER_PROJECT_ID=*. It picks up new projects as you create them, with no restart and no id to copy.
To give a project workers of its own, add a claim in the project's Orchestration settings. Copies of one claim's worker always share a project, so:
- More traffic for a project → more replicas on that project's claim.
- Another project with its own workers → another claim.
Because your routes own the whole URL path space, projects are told apart by hostname, not by path prefix:
- The shared worker answers on every host. Two projects that use the same path on the bare domain collide, and only one of them answers.
- A project's own worker is reached only on the project's host. Set the base domain in Instance settings → Hosting and a subdomain in Project settings → General, and point that name's DNS at Traefik. A claim that serves APIs is refused for a project with no subdomain.
How many workers you may run at all depends on your edition. See Workers per edition.
What a worker runs
By default a worker does everything: it serves your project's HTTP routes and it runs that project's background workflows. That is the right setup for most deployments, and it needs no configuration.
Pick a claim's type when you want to separate the two. The orchestrator passes it to the claim's workers as WORKER_MODE, so don't set that yourself:
Type (WORKER_MODE) | Serves routes | Runs workflows |
|---|---|---|
both (default) | yes | yes |
route | yes | no |
workflow | no | yes |
Splitting them is worth doing when background work is heavy enough to compete with traffic: give workflows their own worker service and the request workers stop sharing a CPU budget with a five-minute report build. It is the same worker image either way — only the setting differs. The non-commercial edition's two workers are enough for exactly this: one route and one workflow.
IMPORTANT
All workers for one project must use the same mode. A worker that finds another mode already running for its project refuses to start and says so, rather than joining in and running every workflow twice. To split a project, change every worker of that project at once.
An unrecognised value is rejected at boot instead of being treated as the default — a typo should not silently change what a machine runs.
Running triggers on their own workers
Triggers belong to groups. Pick groups on a workflow claim and its workers run only the triggers in those groups (the orchestrator passes them as WORKER_GROUP_ID). A claim with no groups runs every group — the default.
This is how a busy trigger gets machines of its own: put it in a group, and give that group a claim of its own. Routes and queued jobs are not affected by this setting; it only decides which triggers a worker listens to.
NOTE
Moving a trigger to another group moves it between workers: the old worker stops reading it and the new one starts.
Experimental CPU-stall protection
Compiled workers normally run with no execution-watchdog overhead. To enable CPU-stall containment for one project, save this project setting through the admin API:
experimental.workerTimeouts.enabled = trueThe setting is published through NATS and takes effect without redeploying or restarting the container. When enabled, each route's timeoutSeconds defaults to 30 seconds and may be increased. A route is terminated only when its event loop is blocked past that budget; long asynchronous work remains running while the execution process continues to heartbeat. The supervisor then replaces only the isolated execution process, so the container and its NATS watcher stay up.
Request body size
Every buffered request body a route receives is capped. The default is 8 MB, and a request above it is rejected with 413 before any workflow runs. A route that pipes application/octet-stream bodies is not buffered and is not counted against this cap; it forwards the stream instead.
# Kilobytes. 8192 = 8 MB (the default), for buffered bodies.
WORKER_MAX_STREAM_SIZE=8192Set it in the same .env the workers read (see Step 1), or per service in your compose file. Raise it deliberately: a buffered body is held in memory while the route runs, so the cap multiplied by your concurrency is memory the worker has to have. Pipe-stream routes avoid that memory cost but must consume the stream before the request ends.
Fluxify is an API server, not an upload service
Routing large files through your API means the upload is paid for twice — once into the worker, once out of it — and every megabyte competes with the requests you actually want to serve.
For file uploads, have your client upload directly to object storage with a pre-signed URL: your route issues a short-lived signed S3 (or compatible) URL, the client PUTs the file straight to storage, then calls your API again with the resulting object key. Only the key ever passes through Fluxify.
Scheduled run limit
A Trigger Workflow block can hold a run for later. Each held run is kept by NATS until its time comes, so how far ahead a run may be scheduled is a deployment decision. The default is 30 days; a block asking for a later time fails with an error.
# How far ahead a run may be scheduled. Hours, minutes or seconds: 720h = 30 days (the default).
WORKER_SCHEDULE_MAX_HORIZON=720hWrite it as s, m and h — there is no day unit, so a year is 8760h. A value that cannot be read stops the worker at startup. Set it on the workers, in the same .env they read (see Step 1).
There is no cap on how many runs may be waiting. Each one is a small message on your NATS server, stored for as long as it waits. If you allow a long limit on a busy system, size your NATS storage for it: the number of runs you expect to be waiting at once, times the size of the data each one carries.
Audit log retention
With an Enterprise or non-commercial licence, Audit Logs record who changed what. Entries are deleted once they are older than 30 days. The cleanup runs once a day.
# Days an audit log entry is kept before it is deleted (default 30).
AUDIT_LOG_MAX_AGE_DAYS=30Set it on the admin service, like the other retention setting below. Workers don't read it. Entries travel through NATS first, so JetStream has to be on (it already has to be); if NATS is down, the change being logged fails instead of going unrecorded.
Execution history retention
A route or workflow with Execution history turned on saves every run: each block's input, output, timing and whether it succeeded. Saved runs are deleted once they are older than 30 days. The cleanup runs once a day.
# Days a recorded run is kept before it is deleted (default 30).
RECORDING_MAX_AGE_DAYS=30Set it on the admin service, in the same .env it reads (see Step 1). Workers don't read it. Leaving it unset gives you the default.
Saved runs are not masked
Execution history is a debugging tool. Request bodies, values read from app config (secrets too) and responses from other services are saved exactly as the run saw them. Anyone who can open the run history can read them. Turn it on while you debug, then off again.
A worker hands each saved run to the admin through NATS. While the admin is down, NATS keeps the waiting runs for up to a day and up to 512 MB; past that, the oldest are dropped. Size your NATS storage with that in mind if many routes record at once.
Sandbox limit
Each user can keep a limited number of persistent sandboxes in each project. Once they reach it, creating another is refused until they delete one, and the sandbox list shows how many they have, as N / max. Ephemeral runs (run_blocks) save nothing and are never counted.
# Persistent sandboxes one user may keep in one project (default 20).
MAX_SANDBOX_PER_USER=20Set it on the admin service, in the same .env it reads (see Step 1). Workers don't read it. Only a whole number between 1 and 1000 is accepted; anything else falls back to the default of 20.
Plugins
A project can add a plugin four ways: upload the .tgz that fluxify-cli builds, pick an exact version from npm, give a link to a .tgz, or pick a folder. A plugin waits until a creator approves it; nothing runs before that.
- Uploads and links are stored in NATS (the same server that holds your compiled routes), so size its storage for the plugins you keep: each upload is limited to 25 MB and 100 MB unpacked. No setting is needed.
- Links only work for hosts an instance admin trusts. An instance admin adds them through the instance settings API (see Adding a plugin): a host like
plugins.example.com, or*.example.comfor all its subdomains (example.comitself needs its own entry). Onlyhttps://links are accepted. With the list empty, no link can be used. A creator also has to approve each download before Fluxify fetches it. - npm needs the admin to reach the npm registry and
cdn.jsdelivr.net. A version must be exact (@acme/[email protected]), and it follows the project's package release-age rule. - Folders need a folder of built plugins, one subfolder each. The compose file mounts
PLUGINS_HOST_DIR(default./pluginsnext to the compose file) read-only at/pluginson the admin and the development worker:
# Host folder with built plugins, one subfolder per plugin.
PLUGINS_HOST_DIR=./pluginsThe workers the orchestrator starts get the same folder, read-only, when PLUGINS_HOST_DIR is an absolute path on the Docker host (for example /srv/fluxify/plugins). With the default relative path they get no folder, and a plugin added from a folder fails to install on them with "the plugins folder is not mounted on this worker". Upload, npm and link plugins work either way.
AI assistant traces
Send a trace of every AI assistant run to Phoenix, Langfuse or any viewer that reads OpenInference. You see each run, its model calls and its tool calls with timings and token counts. Off by default.
LLM_TRACING_ENABLED=true
# Phoenix on the Docker host. Use http://localhost:6006/v1/traces outside Docker.
LLM_OTLP_TRACES_ENDPOINT=http://host.docker.internal:6006/v1/traces
# Optional. key:value pairs split by ;
LLM_OTLP_TRACES_HEADERS=
# Share of runs traced, 0 to 1 (default 1).
LLM_TRACING_SAMPLE_RATE=1
# Default false: prompts, messages and tool inputs/outputs stay out.
LLM_TRACING_RECORD_CONTENT=falseSet them in the .env the AI assistant's service reads (see Step 1). See Trace the AI assistant for what shows up.
Content can hold secrets
With LLM_TRACING_RECORD_CONTENT=true the traces carry every prompt and tool result as the assistant saw it: user data, and secrets such as password hashes from run history. Anyone who can open the viewer can read them. Keep it false outside your own machine.
Local async executor
Compiled workers include a bounded local executor for the future async-trigger and workflow runtime. It is currently an internal capability rather than a route setting or public scheduling API. It queues only I/O-oriented detached work in the same execution process; it does not isolate CPU-heavy work.
Configure its per-worker bounds through environment variables:
# Defaults shown. A full executor returns 429 for a new async submission.
ASYNC_EXECUTOR_MAX_IN_FLIGHT=10
ASYNC_EXECUTOR_MAX_QUEUE_DEPTH=100
# Graceful shutdown waits this long for accepted work before the process exits.
ASYNC_EXECUTOR_DRAIN_TIMEOUT_MS=30000Future route-to-route, webhook, cron and message-bus adapters use this same submission boundary. Distributed workflows will use durable JetStream scheduling instead of relying on the process-local queue.
Admin API rate limit
The admin API — everything the portal calls, plus login and user management — is capped per signed-in user. The default is 15 requests per second. A user over the limit gets a 429 with a Retry-After: 1 header, and the very next second they are served normally again. The portal waits and retries a 429 for you (up to 3 times), so a short burst does not show an error.
# Admin API requests allowed per second, per user. 0 turns the limit off.
ADMIN_RATE_LIMIT_PER_SEC=15Set it on the admin service, in the same .env it reads (see Step 1). Leaving it unset gives you the default, so an existing deployment needs no change.
This is a guard rail for the control plane, not a traffic policy for your projects:
- Your API routes are not affected. Only the admin surface is counted; requests to the routes you build are never capped by this setting.
- It is counted per signed-in user, so one busy account cannot slow the admin API down for your teammates.
- Signed-out requests are not counted. Behind a reverse proxy every signed-out visitor looks like the same caller, so counting them would mean one visitor could lock out the login page for everyone.
Why the admin API has a cap at all
The admin service is the one holding your database connection. A client stuck in a retry loop can otherwise keep it busy enough to slow the portal down for every project on the instance.
Raise it if a team regularly works in pages that load many panels at once; ten per second is comfortable for normal portal use. If your Redis is unreachable the limit is skipped rather than enforced, so a Redis outage cannot lock anyone out of the portal.
Step 1 — Create your .env
The compose file and env.example live in the Fluxify repository, so clone it first. Copy docker/production/env.example to docker/production/.env next to the compose file. The admin, the orchestrator and every worker share the same .env:
git clone https://github.com/Fluxify-rest/Fluxify.git && cd Fluxify
cp docker/production/env.example docker/production/.envAt minimum set these:
#====================== ENVIRONMENT ======================
NODE_ENV=production
ENVIRONMENT=production
#====================== PUBLIC ADDRESS ======================
# The address people open Fluxify at, with the scheme and no trailing slash.
# Better Auth uses it as its base URL. Sign-in only works at this address.
SERVER_URL=https://your-domain.com
# Origins allowed to call the admin API from a different address. Include
# SERVER_URL when the portal is served from it.
TRUSTED_ORIGINS=https://your-domain.com
#====================== DATABASES ======================
PG_URL=postgres://postgres:postgres@postgres:5432/fluxify_alpha
REDIS_HOST=valkey
REDIS_PORT=6379
#====================== EVENT BUS ======================
NATS_URL=nats://nats:4222
NATS_TOKEN=fluxify_nats_token
#====================== SECURITY & KEYS ======================
MASTER_ENCRYPTION_KEY=<openssl rand -base64 32>
BETTER_AUTH_SECRET=<openssl rand -base64 32>
SYSTEM_ACCESS_KEY=<openssl rand -hex 32>
#====================== FIRST-RUN ADMIN ======================
[email protected]
SEED_USER_PASSWORD=ChangeThisPassword123!
SEED_USER_NAME=Admin User
#====================== WORKERS ======================
# The image the orchestrator creates workers from. Use the same tag as admin.
ORCHESTRATOR_WORKER_IMAGE=ghcr.io/fluxify-rest/fluxify-worker:v0.0.3-alpha-pre-pluginWARNING
env.example ships sample keys. Replace MASTER_ENCRYPTION_KEY, BETTER_AUTH_SECRET and SYSTEM_ACCESS_KEY with your own before the first start. Change the Postgres password and NATS_TOKEN too. They are also written in docker-compose.yml (POSTGRES_PASSWORD and NATS's --auth), so change both places to the same value.
WARNING
Back up MASTER_ENCRYPTION_KEY. Losing or changing it after storing data makes every saved credential unreadable. Admin and workers must use the same value — your project's configuration travels to workers encrypted with it.
NOTE
The compose file already sets ENABLE_ADMIN=true on the admin service and keeps the workers as pure executors — you don't need to set those yourself. HOSTNAME is the address the processes listen on inside the container. Leave it at 0.0.0.0; it is not your domain.
Images
The compose file builds admin and the orchestrator from the source you cloned, while the orchestrator pulls workers from ORCHESTRATOR_WORKER_IMAGE. To run released images instead, and keep all three on the same version, swap the build: block for the image: line above it on both services, with the same tag as ORCHESTRATOR_WORKER_IMAGE:
admin:
image: ghcr.io/fluxify-rest/fluxify-admin:v0.0.3-alpha-pre-plugin
orchestrator:
image: ghcr.io/fluxify-rest/fluxify-orchestrator:v0.0.3-alpha-pre-pluginPorts and HTTPS
| Port | What | Open it to the internet? |
|---|---|---|
8080 → Traefik 80 | The dashboard, the admin API and your APIs | Yes. Map it to 80:80 on a real server. |
8081 | Traefik's dashboard, with no password | No. Remove the port, or keep it firewalled. |
Postgres, Valkey, NATS and the workers publish no ports. Keep it that way.
Traefik here serves plain HTTP. For HTTPS, put TLS in front of it: a load balancer or proxy that holds the certificate and forwards to Traefik, or TLS on Traefik's web entry point. Then set the three URLs above to https://.
Generate your secret keys
Use this generator to create secure values for MASTER_ENCRYPTION_KEY and BETTER_AUTH_SECRET, then paste them into your shared .env:
Step 2 — Start the stack
docker compose -f docker/production/docker-compose.yml up -dThis launches Traefik, the admin container, the orchestrator, and the Postgres / Valkey / NATS dependencies. The admin container applies database updates on startup, creates the seed admin, and adds the default claim: one worker that serves every project. Within a few seconds the orchestrator starts that worker.
Check: docker ps shows a container named fluxify-worker-… next to the compose services (plus fluxify-dev-worker, the development worker), and curl http://localhost:8080/_/admin/api/public-settings returns 200.
Stopping the stack
docker compose down stops only the compose services. The fluxify-worker-… containers keep running: the orchestrator starts them on its own, so compose does not know about them, and they are left up on purpose so that restarting the orchestrator never takes your API down. They also come back when Docker restarts.
To stop everything, remove the workers as well:
docker compose -f docker/production/docker-compose.yml down
docker rm -f $(docker ps -aq --filter label=fluxify.managed-by=orchestrator)TIP
Each worker finishes the requests it is answering before it exits, so this is a clean stop, not a crash. Check: docker ps no longer lists any fluxify-worker-… container.
Step 3 — Create your projects
- Open
http://your-domain.com/_/admin/uiand log in with the seed credentials. - Create your projects.
That's it: the default worker serves every project, new ones included, with no id to copy and no restart. Saving a route in the editor publishes it to the workers in place.
To give a project workers of its own, see Which projects a worker serves.
Step 4 — Access
| Surface | URL |
|---|---|
| Dashboard | http://your-domain.com/_/admin/ui |
| Admin API | http://your-domain.com/_/admin/api |
| Your APIs, on the shared worker | http://your-domain.com/ |
| A project's API, on its own workers | http://<project subdomain>.your-domain.com/ |
Connecting AI clients (MCP)
Fluxify has an MCP server, so AI apps and coding agents can work with your projects. Point the client at:
https://your-domain.com/_/admin/mcpThe client signs in as a Fluxify user, and you approve it in the browser. It gets the same access that user has. For sign-in to work, these addresses go to Fluxify on the same port as everything else:
| URL | What it is |
|---|---|
/_/admin/mcp | The MCP server |
/.well-known/oauth-protected-resource | Tells the client where to sign in |
/.well-known/oauth-authorization-server | The sign-in details |
Only paths starting with /.well-known/oauth- are taken for this. Any other /.well-known/... path still reaches your own routes.
IMPORTANT
Cloud AI apps need a public HTTPS address. claude.ai and ChatGPT connect from their own servers, not from your computer, so they can't reach localhost or a private network. Your instance must be reachable from the internet over https://. Apps running on the server itself can use http://localhost:8080.
WARNING
Use HTTPS on any address other than localhost. On such an address, Fluxify tells MCP clients to sign in over https://, even when it is served over plain http://. Without HTTPS, sign-in fails.
Check: replace <url> with your address.
curl -i -X POST <url>/_/admin/mcp # 401, with a WWW-Authenticate header
curl <url>/.well-known/oauth-protected-resource/_/admin/mcp # JSON
curl <url>/.well-known/oauth-authorization-server/_/admin/api/auth # JSONThe 401 is expected: it means the MCP server is up and asks the client to sign in. The JSON lists your public address. If it shows another one, fix SERVER_URL and TRUSTED_ORIGINS in your .env.
Scaling the workers
Raise the replica count on the claim that needs more workers. A project's claims are in its Orchestration settings, and the claim that serves every project is in the instance's. The orchestrator starts the extra workers on its next pass, and Traefik picks them up on its own, with no proxy change needed. Workers hold no state, so you can scale up and down freely, up to the node pool's ceiling.
Each claim also sets how much CPU and memory each of its workers may use. The default is 1 core and 1024 MB, adjustable in steps of 0.5 core and 256 MB. On Docker that is the container's limit. Resizing a claim replaces its workers one at a time. See Sizing a worker for the full ranges.
TIP
Scale out with replicas. Each worker container has one isolated execution process, so replicas add capacity without competing for the same CPU budget.
IMPORTANT
Scale workers, not the admin. Keep a single admin container so database updates and the seed step run exactly once.
Development workers
A worker started with FLUXIFY_ENV=development is a development worker. It runs only development work, never production's, and it serves every project in both modes, so WORKER_PROJECT_ID, WORKER_MODE and WORKER_GROUP_ID are ignored on it. It does not use a license slot (see Workers per edition), and it needs no claim. See Environments.
The compose file starts one for you, as the worker-dev service (container fluxify-dev-worker). It uses the same image as the production workers, ORCHESTRATOR_WORKER_IMAGE, and is not an orchestrator claim: the orchestrator does not create, resize or remove it. It is also the only worker that is a compose service, so --remove-orphans leaves it alone.
- Traefik sends it
/_sandboxand/_/dev, and nothing else. A sandbox is called at/_sandbox/<id>/…, and a route on development at/_/dev/…, on your normal address. That rule sends them here instead of to a production worker, which would answer/_/devwith a404. Every other path,/_/devicesincluded, still goes to the production workers. The rule matches any host, so a project's subdomain reaches the development worker too, and a project with a subdomain answers these paths on that subdomain only. Sandboxes, the playground andcall_routeuse your address, so they need no setting. A development claim answers before this worker does, so the two can run side by side while you move over. - How admin reaches it (optional). The admin calls development workers only through Traefik (
PROXY_URL=http://traefik, set in the compose file) and lets Traefik pick the worker pool byHost; it never calls a single worker. LeavePROXY_URLunset to have admin call the project's own public address instead. It is read by admin; the worker ignores it. - Its settings are fixed. It gets the NATS, Valkey and encryption-key settings from your
.env, and nothing else; in particular no database address. - Don't put
FLUXIFY_ENV=developmentin.env. Admin and every production worker read that file, so it would turn them all into development workers. Leave the setting where the compose file puts it, onworker-devonly. - Don't want it? Delete the
worker-devservice from the compose file. If you still want development work to run, add a development claim for every project instead.
Giving development its own workers
A claim can be made for Development as well as Production — the picker is on the claim form, and Production is what it opens on. A development claim is an ordinary claim: the orchestrator creates, resizes and removes its workers the same way, and what changes is only what those workers are.
| Production claim | Development claim | |
|---|---|---|
| What it serves | Live traffic, on the project's own address | /_sandbox, /_/dev and /_/dev/… only |
| License rules | The edition's worker limit, worker types and per-project rule | None apply, on any edition: no slot is taken, and any type or project is allowed |
| Autoscaling | Grows with load, where your edition allows it | Never — it runs exactly the copies you give it |
| Where you reach it | The project's host, or any address for a claim that serves every project | The development paths, on the same addresses |
- Your edition does not govern one. A development claim takes no license slot, so the edition's worker limit never counts it; and the edition's other rules — which worker types it includes, and whether it allows a worker for a single project — are about production capacity too, so neither is checked. On Community, a project's development claim is created where the same claim in production is refused. The node pool is what limits it, because it is real capacity on your machine: past the pool its copies wait as pending. See Workers per edition.
- A maximum is refused on one. Development capacity is what you asked for by hand, not something load grows into, so the form hides the field.
- A project's development claim needs a subdomain, for the same reason a production one does: development requests are told apart by hostname, so without a subdomain there is nowhere to route them. A claim that serves every project needs none — it answers the development paths on every address.
- Production work is never affected. A development claim serves the same trigger groups in development only, so the production worker that serves those groups keeps them.
- Changing the environment replaces the claim's workers. A worker's environment is fixed when it starts, so the old containers drain and new ones take their place.
TIP
A development claim can replace the fixed development worker. A Development claim for every project answers exactly the same paths as the worker-dev service, and the orchestrator manages it for you: make it in Instance settings → Orchestration, then delete the worker-dev service (Kubernetes: set devWorker.enabled: false). Nothing breaks in between — a project's own development claim answers first, then the claim for every project, and the fixed worker only sees what neither of them covers.
Database integration idle timeout
Compiled workers share database clients across requests. To avoid retaining a pool for an integration that has gone quiet, set INTEGRATION_TIMEOUT_POLICY_IN_SEC in docker/production/.env:
INTEGRATION_TIMEOUT_POLICY_IN_SEC=450450 is the default and means 7.5 minutes. When an integration has no in-flight request or transaction for that period, its PostgreSQL, MySQL, or MongoDB client closes. The next request opens a fresh client. This never closes a client while a request is using it; choose a larger value when avoiding a connection cold-start matters more than releasing idle sockets.
Testing plugin integrations
Test connection, the table list and Inspect for an integration that a plugin adds run on a worker, never on the admin. The admin sends the request to a worker of the same environment: a development worker for development credentials, a production worker for production ones. It never falls back to the other environment. Built-in integrations are still tested by the admin.
# How long the admin waits for a worker's answer, in milliseconds (default 15000).
INTEGRATION_WORKER_TIMEOUT_MS=15000Set it on the admin service. If no worker of that environment is running, the portal says so at once ("No development worker is running for this project"). If one is running but does not answer in time, it says "No response from a development worker in 15s".
Health checks
Point your load balancer and orchestrator at port 5601, not 5600:
| Probe | URL (port 5601) | Means |
|---|---|---|
| Startup | /_/admin/api/healthchecks/startup | The worker process is up |
| Readiness | /_/admin/api/healthchecks/ready | Your routes are loaded — safe to send traffic |
Port 5600 carries your API traffic. A probe sent there can be answered by any one of the worker's internal handlers, so it can't tell you the whole container is healthy. The bundled compose file already targets 5601.
Upgrading
docker compose -f docker/production/docker-compose.yml pull
docker compose -f docker/production/docker-compose.yml up -dBack up Postgres first. Roll the admin first: on startup it updates the database to the new version, then the workers follow automatically. On the default compose file, which builds admin and the orchestrator from source, pull the new source and add --build to up -d instead. Either way, move ORCHESTRATOR_WORKER_IMAGE to the same version.
If a database update fails
The admin stops, and its log says database migration failed with the reason. The update is all or nothing, so the database is left exactly as it was: fix the cause, or go back to the previous version, and start the admin again.
Troubleshooting
No fluxify-worker… container ever appears Check the orchestrator's logs: docker logs fluxify-orchestrator. The usual causes are a missing Docker socket mount, ORCHESTRATOR_WORKER_IMAGE naming an image that cannot be pulled, or ORCHESTRATOR_SEED_DEFAULT_CLAIM=false with no claim added since. A claim that asks for more workers than your edition or the node pool allows waits as pending, and its Orchestration page says why.
Traffic returns 404 for /_/admin pages Traefik routes by path priority. Confirm the admin service still carries its PathPrefix(/_/admin) label and that the container is running.
An MCP client can't sign in, or /.well-known/oauth-… returns 404 The admin service's label must also match PathPrefix(/.well-known/oauth-), as in the compose file. If the JSON from the MCP checks shows an http:// or wrong address, fix SERVER_URL and TRUSTED_ORIGINS, and serve Fluxify over HTTPS.
Workers never receive traffic They stay out of rotation until the readiness check passes. Check a worker's logs — a NATS_TOKEN mismatch or a missing MASTER_ENCRYPTION_KEY is the usual cause. You can hit the probe directly from inside the network at /_/admin/api/healthchecks/ready on port 5601.
Scaling down takes a few seconds longer than you expect That is on purpose. A worker that is being removed first reports itself as not ready, then keeps answering requests for about 10 seconds while the load balancer (which checks every 5 seconds) stops sending it traffic. Only then does it finish its in-flight work and stop, so no request is dropped.
A worker exits immediately on start It refuses to run without WORKER_PROJECT_ID or MASTER_ENCRYPTION_KEY, and says which one is missing in its logs. Both come from your shared .env. A WORKER_MODE that is not route, workflow or both stops it the same way.
A second worker for a project will not start, complaining about a consumer Two workers on one project are set to different modes. See What a worker runs — every worker for a project has to agree. This is deliberate: the alternative is both of them running the same background job.
Routes save fine but never reach the workers NATS needs JetStream enabled (-js) and version 2.14 or newer. The bundled compose file does both; if you brought your own NATS, add the flag and check the version. On an older NATS, Fluxify exits at start saying it is too old.
JetStream also backs two KV buckets, both created on demand — nothing to provision by hand, but they are what the -js flag is for:
| Bucket | Holds |
|---|---|
fluxify_artifacts | Compiled routes and workflows, so a worker runs them without a database. |
fluxify_config | Instance settings and other cross-node config, so a flag change reaches every process. |
Audit events also use a stream of their own, FLUXIFY_AUDIT, created on demand. If it fills up because the admin has been down for a long time, changes that need logging fail until the admin catches up, so nothing goes unrecorded.
fluxify_config is a distribution layer, not storage — Postgres remains the source of truth, and the admin container rebuilds the bucket from it on every start. Losing the NATS volume is therefore recoverable: restart admin and the bucket comes back.
The admin container exits at start complaining about instance settings Config is a hard dependency, not a degradable one, so a process that cannot reach NATS KV exits rather than come up half-configured and authenticate people against nothing. Check NATS_URL, NATS_TOKEN, and that JetStream is enabled.
Traefik can't see the services Traefik reads Docker labels through the mounted Docker socket. Ensure the socket volume is present and the services share the same network as Traefik.
