This commit is contained in:
2026-10-13 10:02:20 +02:00
parent a93f865e91
commit 80e595c02e
24 changed files with 2210 additions and 4310 deletions
+128 -179
View File
@@ -1,238 +1,187 @@
# Configuration
Every setting comes from the environment, or from an env file. Which file
depends on how the instance was started:
Most installations can keep the defaults. Settings come from environment
variables and an env file:
| Started with | Reads |
| How you start Fluksio | Env file |
|---|---|
| `fluksio serve` | `env` inside the data directory (`$FLUKSIO_ENV_FILE`) |
| the Docker stack | `.env` beside `docker/` |
| `fluksio serve` | `<data-dir>/env`, unless `FLUKSIO_ENV_FILE` names another file |
| Docker with the Makefile | `.env` in this repository's root |
Anything already exported wins over the file.
Exported variables override the file. `fluksio serve` flags override both.
For CLI installations, select storage with `--data-dir`, `FLUKSIO_HOME` or
`--global`; see [The fluksio command](../code/cli.md).
## Storage
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `DATA_DIR` | `flow-data` (`./.fluksio` via the CLI; `~/.fluksio` with `--global`) | everything below derives from this |
| `DATABASE_URL` | SQLite in `DATA_DIR` | any SQLAlchemy URL |
| `FLOWS_DIR` | `$DATA_DIR/flows` | the git repository holding flows |
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials, kept outside the repo |
| `ALERTS_FILE` | `$DATA_DIR/alerts.json` | channels and rules |
| `PROVISIONERS_FILE` | `$DATA_DIR/provisioners.json` | clusters a machine can be started from; absent means none |
| `DATA_DIR` | `flow-data`; CLI uses `./.fluksio` or `~/.fluksio` with `--global` | base directory for instance data |
| `DATABASE_URL` | SQLite at `$DATA_DIR/fluksio.db` | SQLite URL; other database engines are unsupported |
| `FLOWS_DIR` | `$DATA_DIR/flows` | flow git repository |
| `SECRETS_FILE` | `$DATA_DIR/secrets.enc` | encrypted credentials |
| `ALERTS_FILE` | `$DATA_DIR/alerts.json` | alert channels and rules |
| `WEBPUSH_FILE` | `$DATA_DIR/webpush.json` | push keys and browser subscriptions |
| `PROVISIONERS_FILE` | `$DATA_DIR/provisioners.json` | optional worker provisioning configuration |
| `PANELS_FILE` | `$DATA_DIR/panels.json` | wall-panel pairings |
| `OAUTH_PRIVATE_KEY_FILE` | `$DATA_DIR/oauth-key.pem` | signs agent and worker tokens |
| `CLOUD_CONFIG_FILE` | `$DATA_DIR/cloud.json` | the portal enrolment, if any |
| `NODE_VENV` | `auto` | which interpreter node code runs on — see below |
| `OAUTH_PRIVATE_KEY_FILE` | `$DATA_DIR/oauth-key.pem` | agent and worker signing key |
| `CLOUD_CONFIG_FILE` | `$DATA_DIR/cloud.json` | optional portal enrollment |
| `AI_CONFIG_FILE` | `$DATA_DIR/ai.json` | optional assistant provider configuration |
| `CHATS_DIR` | `$DATA_DIR/chats` | assistant transcripts |
| `NODE_VENV` | `auto` | interpreter for node code |
Set `DATA_DIR` and the rest follow. Set one explicitly and it wins, which is
what the container images do to pin everything onto `/data`.
Changing `DATA_DIR` moves the derived paths unless you set them individually.
Docker stores instance data under `/data` on the `app-flow-data` volume.
`NODE_VENV` is the exception, being about an environment rather than a path:
| Value | What node code runs on |
| `NODE_VENV` value | Behavior |
|---|---|
| `auto` (default) | the venv Fluksio was installed into, when it was installed into one and there is no venv of its own already built. `pip install fluksio` beside your own packages is this case, and the packages are then already there — the Modules screen turns read-only, because that environment is not Fluksio's to install into. |
| `managed` | a venv the engine builds under `DATA_DIR` and owns, which the Modules screen installs into with `uv pip sync`. The container images set this: the venv in them holds the app and nothing of anybody else's. |
| a path | that interpreter, or that venv, whatever it is. |
| `auto` | keeps an existing managed venv; otherwise uses the installation's venv when available, or creates a managed one |
| `managed` | creates a venv under `DATA_DIR`; the Modules screen manages its packages |
| interpreter or venv path | uses the specified Python environment |
An instance that already has a managed venv keeps it on upgrade under
`auto`, because it may hold packages somebody installed on purpose.
Docker uses `managed`. When Fluksio adopts your own venv, the Modules screen
is read-only; install packages into that environment yourself.
!!! warning "The four files that must be on persistent storage"
Back up the whole data directory and the persistent `SECRET_KEY`. In Docker,
keep `.env` and `app-flow-data`; back up `app-redis-data` too when preserving
live state. Compose prefixes volume names with the project name `fluksio-app`.
Keep runtime files on persistent storage so rebuilds preserve credentials,
pairings and subscriptions.
`secrets.enc`, `alerts.json`, `panels.json` and `oauth-key.pem` are written
at runtime. In a container, anything not on a volume lands in the writable
layer and is lost on the next rebuild, un-pairing every screen and
revoking every agent.
## Live state
## State
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `REDIS_HOST` | unset | without it, flow state lives in memory and does not survive a restart |
| `REDIS_PORT` | `6379` | |
| `REDIS_HOST` | unset | unset uses memory; a Redis host enables persistent flow state |
| `REDIS_PORT` | `6379` | Redis port |
Flow state is the last value of every message, node memory, and the run queue.
Redis here is persistence, not a cache. Run it with append-only persistence on
(`--appendonly yes`, which the compose stack sets).
Live state includes message values, node memory and queued work. In-memory
state is lost on restart. The Docker stack uses Redis with append-only
persistence on its own `app-redis-data` volume. Its default sync policy may
lose about a second of recent writes after an unclean shutdown.
That writes with Redis's default `appendfsync everysec`, so an unclean stop of
Redis or of its host loses up to a second of queued work. `appendfsync always`
closes the window and cuts engine throughput to about a third, so the default
stands.
## Accounts and access
## Identity and access
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `SECRET_KEY` | generated | signs sessions and derives the secrets-store key |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `11520` (8 days) | |
| `FIRST_SUPERUSER` | — | absent means the CLI creates one on first run |
| `FIRST_SUPERUSER_PASSWORD` | — | absent means one is generated and printed once |
| `DOMAIN` | `localhost` | what the API and OAuth issuer are built from |
| `FRONTEND_HOST` | `http://localhost:5173` | used in mails, OAuth metadata and panel pairing links |
| `BACKEND_CORS_ORIGINS` | `[]` | comma-separated; `FRONTEND_HOST` is always allowed |
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` (`127.0.0.1,172.16.0.0/12` in the compose stack) | proxies whose `X-Forwarded-For` is believed, as uvicorn reads it; the sign-in, sign-up and OAuth limits count the rightmost hop none of them added |
| `SECRET_KEY` | generated | signs sessions and encrypts the secrets store; the CLI persists it and `make init` writes it to `.env` |
| `ACCESS_TOKEN_EXPIRE_MINUTES` | `11520` (8 days) | session-token lifetime |
| `FIRST_SUPERUSER` | unset | initial administrator email; the CLI creates an account if needed |
| `FIRST_SUPERUSER_PASSWORD` | unset | initial password; the CLI generates and prints one if needed |
| `DOMAIN` | `localhost` | API hostname and OAuth issuer; Docker also uses it for routing |
| `FRONTEND_HOST` | `http://localhost:5173` | interface URL for links and metadata |
| `BACKEND_CORS_ORIGINS` | `[]` | allowed origins, as comma-separated URLs or a JSON list; `FRONTEND_HOST` is also allowed |
| `FORWARDED_ALLOW_IPS` | `127.0.0.1` | trusted proxies; Docker adds `172.16.0.0/12` |
| `SIGNUP_ENABLED` | `false` | allows account creation from the login page; otherwise use **Admin → Users** |
## Where the interface looks for the API
!!! warning "Keep your secret key"
The dashboard is a static bundle, so this one is a **build** argument of the
`frontend` image rather than a setting the running stack reads.
Changing `SECRET_KEY` signs everyone out and makes existing encrypted
secrets unreadable. Back it up with your data.
| Argument | Used by | Effect |
|---|---|---|
| `VITE_API_URL` | `frontend` at build time | the address the interface calls |
## Interface API address
The compose stack takes it from `.env`, falling back to `https://api.${DOMAIN}`.
`scripts/setup.sh` writes it there with the scheme `ENVIRONMENT` implies, so a
local build calls `http://` and does not fail a certificate check nothing is
there to satisfy.
`VITE_API_URL` is a frontend **build argument**, so changing it requires
rebuilding the frontend image.
Empty is the useful value: the interface then addresses the API relative to
whichever origin served the page, so one image answers on a hostname, on a
`http://<host-ip>:<port>`, and through an ssh tunnel alike, and no origin has
to be added to `BACKEND_CORS_ORIGINS`, because there is only one.
`docker/compose.lan.yml` builds it that way and puts an `/api` proxy in front
of the backend to complete it; see
[getting started](../getting-started/facility-automation.md#on-your-own-network-by-address).
| Setup | Value |
|---|---|
| Production Docker | set `.env` to `https://api.<your-domain>` |
| `make dev-local` | overlay sets `http://api.localhost` by default |
| `make dev-lan` | overlay sets an empty value, using `/api` on the page's own origin |
Set it to an absolute URL only when the API genuinely lives somewhere else, and
remember it is fixed at build time: changing it means rebuilding that image.
Without an explicit value, the production Compose file falls back to
`https://api.${DOMAIN}`. Set `FRONTEND_HOST` and `BACKEND_CORS_ORIGINS` to
match your deployment as well; `make init` generates secrets, not deployment
URLs.
!!! danger "Rotating `SECRET_KEY`"
The secrets store is encrypted with a key derived from it. Change it and
the store stops decrypting, and every session is signed out. Re-enter your
secrets, or plan the rotation properly.
The LAN overlay also configures nginx to proxy `/api` and WebSocket requests
to the backend. An empty value needs that proxy when the frontend and backend
are served separately. See [facility automation](../getting-started/facility-automation.md#run-it-long-term)
for deployment settings.
## Environment
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `ENVIRONMENT` | `local` | `local`, `staging` or `production` |
| `PRIVATE_API_ENABLED` | `false` | unauthenticated test-only endpoints; needs `ENVIRONMENT=local` too |
| `API_DOCS_ENABLED` | `false` | serves `/docs`, `/redoc` and the OpenAPI document to any caller; otherwise only to one on this machine. Never in `production` |
| `SIGNUP_ENABLED` | `false` | lets anyone who reaches the login page create an account; otherwise a superuser adds them under **Admin** |
| `TZ` | `UTC` | the timezone every schedule is written in |
| `PRIVATE_API_ENABLED` | `false` | test endpoints; also requires `ENVIRONMENT=local` |
| `API_DOCS_ENABLED` | `false` | opens `/docs`, `/redoc` and `/api/v1/openapi.json` to network callers outside production |
| `TZ` | host timezone; Docker defaults to `UTC` | timezone used by cron schedules |
`TZ` is the container's own, not a setting the code reads: an `inject` or a
`delay` with a cron expression fires on local time. Left at `UTC`, "off at
02:00" means two in the morning UTC, which in most of the world is neither two
o'clock nor the same hour in summer as in winter. Set it to where the
instance is.
API documentation is available to loopback callers outside production.
`API_DOCS_ENABLED=true` opens it to other callers, as the Docker development
stack does. Production disables it entirely. Staging and production also
refuse `changethis` values for the session key or administrator password.
`production` closes `/docs`, `/redoc` and the OpenAPI document, because the
schema enumerates every endpoint the instance serves, including the paths
webhook nodes mounted at runtime. Elsewhere they answer a caller on the
engine's own machine, so `fluksio serve --host 0.0.0.0` does not hand them to
the network; `API_DOCS_ENABLED` opens them to everyone, as the dev stack does. It also turns a `changethis` secret from a
warning into a refusal to start.
Set `TZ` in `.env` for schedules that should follow local time, such as
`Europe/Berlin`.
## The engine
## Engine and resources
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `FLOW_MAX_WORKERS` | `4` | node-code subprocesses run in parallel |
| `FLOW_MAX_CASCADES` | `4` | cascades in flight at once; throughput is this over the mean cascade time, so raise it where nodes wait on a network rather than a CPU |
| `FLOW_MAX_RUNS` | `4` | batch runs driven at once. A different limit from the one above: a run drives a whole graph, and its nodes are bounded by `FLOW_MAX_WORKERS`. This is what a sweep queues behind |
| `FLOW_NODE_TIMEOUT` | `0` | seconds a node may be silent, unless it sets its own; 0 is no limit |
| `FLOW_CPUS` | `0` | cores nodes that declare `resources` may be given; 0 works it out as every core but two, which are what keeps the engine answering while the machine is busy |
| `FLOW_GPUS` | `0` | GPUs on this machine, each held by one node at a time. 0 counts NVIDIA's device nodes; say a number for anything they miss |
| `FLOW_RAM` | `0` | megabytes nodes that declare `ram` may be given; 0 works it out as the machine's memory, or the container's limit where that is lower, less a gigabyte for the engine |
| `FLOW_WORKER_MAX_RSS_MB` | `0` | a warm worker holding more resident memory than this when its node returns is retired rather than kept; 0 keeps every worker warm |
| `OBS_RETENTION_DAYS` | `30` | how long metrics, events and run records are kept |
| `ARTIFACT_GC_INTERVAL_S` | `3600` | how often artifact bytes nothing refers to are swept away; 0 never sweeps |
| `ARTIFACT_GC_GRACE_S` | `3600` | how long a freshly written artifact is spared, whatever refers to it |
| `MAX_ARTIFACT_BYTES` | `2147483648` | the largest body `PUT /artifacts` will take; 0 removes the limit |
| `ARTIFACT_VOLATILE_DIR` | worked out | where frames a flow only shows live are held; empty picks a directory under `/dev/shm` named for the data directory, and falls back to the temporary directory |
| `ARTIFACT_VOLATILE_BYTES` | `50331648` | how much that ring holds before the oldest frames fall out; 0 turns it off and volatile saves land in the store |
| `FLOW_MAX_WORKERS` | `4` | local node-code subprocesses |
| `FLOW_MAX_CASCADES` | `4` | concurrent live message cascades |
| `FLOW_MAX_RUNS` | `4` | concurrent batch runs |
| `FLOW_NODE_TIMEOUT` | `0` | maximum node silence in seconds, unless the node overrides it; `0` disables |
| `FLOW_CPUS` | `0` | CPU budget; `0` detects cores and reserves two for the engine |
| `FLOW_GPUS` | `0` | local GPU count; `0` detects NVIDIA device nodes |
| `FLOW_RAM` | `0` | memory budget in MB; `0` detects host/container memory and reserves 1 GiB |
| `FLOW_WORKER_MAX_RSS_MB` | `0` | retire a local worker above this memory use after a call; `0` disables |
| `OBS_RETENTION_DAYS` | `30` | retention for metrics, events and run records |
The three concurrency limits are also flags on `fluksio serve`
(`--max-workers`, `--max-cascades`, `--max-runs`), as are the card count and
memory, `--gpus` and `--ram`, and the worker ceiling, `--worker-max-rss`. The flags
outrank the file, and the engine says which numbers it started with in its
first lines. Each pool size must be at least 1 and the others at least 0:
a number below that is refused as a flag error naming it, rather than read as
the default. Leave one empty (or unset) to get the default.
The three concurrency limits must be at least `1`. They also have CLI flags:
`--max-workers`, `--max-cascades` and `--max-runs`. Resource flags are `--gpus`,
`--ram` and `--worker-max-rss`.
An artifact is referred to by a run that recorded it or by a message currently
holding it; anything else is what a camera published four hours ago, and the
sweep is what keeps a flow streaming media from filling the disk. It stands
aside entirely while a run is in flight, since a node may store a checkpoint
long before it returns the reference to it.
Nodes with declared resources are scheduled against the available budget.
Undeclared nodes use the shared pool and receive thread caps. Attached workers
report their own inventory; the engine's resource settings describe only its
own machine. See [Remote workers](../code/workers.md) and the **Workers** screen.
Frames saved with `volatile=True` skip all of that. They go to a ring in memory
instead of the volume, the oldest falling out once the newest need the room,
and the engine pushes them down the websocket to whichever screens are drawing
them — which is what a camera at ten frames a second needs and the store cannot
give it. A frame a run *records* is copied into the store on the way, so
returned media is kept and emitted media is not. Under Docker the ring lives in
the container's `/dev/shm`, whose default is 64 MB: raise `shm_size` alongside
`ARTIFACT_VOLATILE_BYTES`.
## Artifacts
A node that declares nothing is not accounted against `FLOW_CPUS`; it runs on
the shared pool and is given `FLOW_CPUS / FLOW_MAX_WORKERS` as a thread cap, so
several at once cannot each size themselves to the whole machine. Setting
`OMP_NUM_THREADS` (or any of its siblings) on the engine yourself overrides
that default.
| Variable | Default | Purpose |
|---|---|---|
| `MAX_ARTIFACT_BYTES` | `2147483648` (2 GiB) | upload limit; `0` removes it |
| `ARTIFACT_GC_INTERVAL_S` | `3600` | cleanup interval; `0` disables |
| `ARTIFACT_GC_GRACE_S` | `3600` | minimum age before cleanup |
| `ARTIFACT_VOLATILE_DIR` | auto-detected | live-media directory under `/dev/shm`, or the temporary directory |
| `ARTIFACT_VOLATILE_BYTES` | `50331648` (48 MiB) | live-media ring size; `0` stores volatile saves durably |
Workers are kept warm between calls, and a library like JAX keeps what it
allocated for as long as its process lives — so a worker that fitted a model
can sit on gigabytes after its run is over, on the shared pool as much as on a
declared one. `FLOW_WORKER_MAX_RSS_MB` retires such a worker when its node
returns, and the next call pays its imports again. That bounds idle memory
only; what a node holds while it runs is accounted, and only if it declared
`ram`.
`FLOW_CPUS`, `FLOW_GPUS` and `FLOW_RAM` are this machine's figures. An attached worker reports its own when
it dials in, and a node goes to whichever machine can grant what it asked for,
so a GPU on a worker needs no `FLOW_GPUS` here. What every machine has free, and
which nodes are queued, is `GET /api/v1/workers/resources` and the Workers
screen. Named sizes live in the database and are `GET /api/v1/flavors`;
`PROVISIONERS_FILE` is where machines can be started from. See
[declaring resources](../getting-started/data-science.md#declaring-what-a-node-needs).
Cleanup removes durable artifacts no longer referenced by recorded runs or
current messages, and pauses while a run is active. Volatile artifacts use a
bounded ring; the oldest disappear as it fills. Recorded run outputs are
copied into durable storage. If you increase the ring size in Docker, increase
`shm_size` too.
## Agents
| Variable | Default | Notes |
| Variable | Default | Purpose |
|---|---|---|
| `MCP_ENABLED` | `false` | opens the `/mcp` endpoint **and** OAuth client registration |
| `MCP_TOKEN_EXPIRE_MINUTES` | `60` | an agent's token is a bearer secret held by a program |
| `MCP_REFRESH_EXPIRE_DAYS` | `30` | |
| `OAUTH_CODE_EXPIRE_SECONDS` | `60` | |
| `MCP_ENABLED` | `false` | enables `/mcp` and OAuth client registration; `.env.example` sets it to `true` |
| `MCP_TOKEN_EXPIRE_MINUTES` | `60` | access-token lifetime |
| `MCP_REFRESH_EXPIRE_DAYS` | `30` | refresh-token lifetime |
| `OAUTH_CODE_EXPIRE_SECONDS` | `60` | authorization-code lifetime |
See [Agents over MCP](../code/agents.md).
## Mail
## Mail and monitoring
Needed for password-reset mails. Without `SMTP_HOST` and `EMAILS_FROM_EMAIL`,
mail is simply off.
Password-reset mail requires `SMTP_HOST` and `EMAILS_FROM_EMAIL`.
| Variable | Default |
|---|---|
| `SMTP_HOST` | — |
| `SMTP_HOST` | unset |
| `SMTP_PORT` | `587` |
| `SMTP_USER` / `SMTP_PASSWORD` | — |
| `SMTP_USER` / `SMTP_PASSWORD` | unset |
| `SMTP_TLS` / `SMTP_SSL` | `true` / `false` |
| `EMAILS_FROM_EMAIL` | — |
| `EMAILS_FROM_NAME` | `Fluksio` |
| `EMAILS_FROM_EMAIL` | unset |
| `EMAILS_FROM_NAME` | `PROJECT_NAME`, normally `Fluksio` |
| `EMAIL_RESET_TOKEN_EXPIRE_HOURS` | `48` |
| `SENTRY_DSN` | unset; enables error reporting when configured |
## Monitoring
| Variable | Default | Notes |
|---|---|---|
| `SENTRY_DSN` | — | error reporting, if you want it |
## Health check
`GET /api/v1/utils/health/` is a *deep* check: it fails when the event loop is
wedged or the state backend is gone, not just when the process is up. That is
what the container healthcheck probes, and what an autoheal sidecar restarts
on.
## See also
- [The `fluksio` command](../code/cli.md) — what the data directory holds
- [Getting started: facility automation](../getting-started/facility-automation.md)
`GET /api/v1/utils/health/` checks the event loop and state backend. Docker
uses it as a health check; `make up` also starts the autoheal service to restart
an unhealthy backend.