The rate the media dtypes could carry was one frame every second or two: each
was a file on the data volume, an event on the socket, and a request back for
the bytes. This closes both halves of that, and they are one feature.
`save_artifact(..., volatile=True)` writes to a `VolatileStore` — the same
content-addressed store, in `/dev/shm`, bounded by size with the oldest falling
out (`ARTIFACT_VOLATILE_BYTES`, 48 MB under the container's raised `shm_size`).
Nothing sweeps it: a frame nobody kept is not worth walking the store to find.
`ArtifactStore.path` falls through to it, which is what lets a volatile frame be
an ordinary reference everywhere else — the dtype check, a panel's digest scope,
`load_artifact` in a node, and the widget's own fetch all work on one unchanged.
`adopt` copies one into the store when a run records it, so "returned media is
kept, emitted media is not" stays true.
The bytes then go down the flows websocket as a length-prefixed binary frame,
sent just ahead of the `message_value` naming them, so a tile has the frame when
it hears the value moved. Nothing is pushed unasked: a client names the messages
it is drawing (`{"type":"media","names":[…]}`), a panel's list is intersected
with the scope it already had, and only the newest frame per name in a batch is
sent — a client that fell behind is not handed frames it would draw over. The
tunnel relays text only, so a screen reached through a portal falls back to
fetching, which is why the rate table now has two rows.
Around the edges: the remote worker's fetch cache is bounded at last
(`FLUKSIO_ARTIFACT_CACHE_BYTES`), since content addressing means nothing in it
ever expires and a media stream fills it with chunks nothing asks for twice; a
port carrying an image draws the frame in the node panel rather than only
saying `image/png · frame.png · 1.79kB`; and an edge chip says that much instead
of a line of hash. The media screenshot stops waiting for `networkidle` — a
camera is a socket that never goes quiet, which is the point of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YC4u66vjzW54fnHu5Juhh9
11 KiB
Configuration
Every setting comes from the environment, or from an env file. Which file depends on how the instance was started:
| Started with | Reads |
|---|---|
fluksio serve |
env inside the data directory ($FLUKSIO_ENV_FILE) |
| the Docker stack | .env beside docker/ |
Anything already exported wins over the file.
Storage
| Variable | Default | Notes |
|---|---|---|
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 |
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 |
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.
NODE_VENV is the exception, being about an environment rather than a path:
| Value | What node code runs on |
|---|---|
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. |
An instance that already has a managed venv keeps it on upgrade under
auto, because it may hold packages somebody installed on purpose.
!!! warning "The four files that must be on persistent storage"
`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.
State
| Variable | Default | Notes |
|---|---|---|
REDIS_HOST |
unset | without it, flow state lives in memory and does not survive a restart |
REDIS_PORT |
6379 |
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.
Identity and access
| Variable | Default | Notes |
|---|---|---|
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 |
Where the interface looks for the API
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.
| Argument | Used by | Effect |
|---|---|---|
VITE_API_URL |
frontend at build time |
the address the interface calls |
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.
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.
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.
!!! 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.
Environment
| Variable | Default | Notes |
|---|---|---|
ENVIRONMENT |
local |
local, staging or production |
PRIVATE_API_ENABLED |
false |
unauthenticated test-only endpoints; needs ENVIRONMENT=local too |
TZ |
UTC |
the timezone every schedule is written in |
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.
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. It also turns a changethis secret from a
warning into a refusal to start.
The engine
| Variable | Default | Notes |
|---|---|---|
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. Not detected — say how many there are |
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 |
The three concurrency limits are also flags on fluksio serve
(--max-workers, --max-cascades, --max-runs), as is the card count,
--gpus. 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 card count 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.
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.
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.
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.
These two 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.
Agents
| Variable | Default | Notes |
|---|---|---|
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 |
See Agents over MCP.
Needed for password-reset mails. Without SMTP_HOST and EMAILS_FROM_EMAIL,
mail is simply off.
| Variable | Default |
|---|---|
SMTP_HOST |
— |
SMTP_PORT |
587 |
SMTP_USER / SMTP_PASSWORD |
— |
SMTP_TLS / SMTP_SSL |
true / false |
EMAILS_FROM_EMAIL |
— |
EMAILS_FROM_NAME |
Fluksio |
EMAIL_RESET_TOKEN_EXPIRE_HOURS |
48 |
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
fluksiocommand — what the data directory holds - Getting started: facility automation