Files
fluxer/fluxer_docs/src/content/docs/operator/configuration.mdx
T

2265 lines
145 KiB
Plaintext

---
# SPDX-License-Identifier: AGPL-3.0-or-later
title: Configuration
description: Every variable a self-hosted instance reads, grouped by concern.
---
:::tip[The Operator Pass is coming]
The [Operator Pass](https://fluxer.app/pass) gives you a direct line to the Fluxer team. It goes on sale in early November 2026 for $199 or €199, paid once. The price goes up on 1 January 2027. Pass holders get these perks for life.
- A private channel in Fluxer Labs where the team answers questions about your instance
- The Operator role in Fluxer Labs
- An Operator Pass profile badge
- Invitations to test new features on your instance
- Your avatar and name on the Fluxer website, if you want
The pass unlocks nothing in the software, and self-hosting stays free. If you have Visionary, you already have the pass.
We are grateful to everyone supporting the project through [Fluxer Plutonium](https://fluxer.app/plutonium) or [donations](https://fluxer.app/donate). All of our code is free and open source on [GitHub](https://github.com/fluxerapp/fluxer).
:::
Fluxer reads its settings from environment variables. They are in a file named `.env`, in the same directory as `docker-compose.yml`.
The installer in [Get started](/operator/get-started/) writes `.env` and fills in every secret. Start with [Core identity and public address](#core-identity-and-public-address) and [Secrets you must generate](#secrets-you-must-generate).
## How configuration is loaded
Compose forwards every setting a self-hosted instance can change from `.env` to each service that reads it. Where the stack needs a value other than the built-in default, Compose sets one and the entry below names it. Any other forwarded name that `.env` leaves out reaches its service empty, and the service applies its built-in default.
[Keys Compose does not forward](#keys-compose-does-not-forward) lists the names `.env` cannot set.
Single-quote values containing a literal `$` so Compose does not expand them as variable references:
```ini
POSTGRES_PASSWORD='ab$cd'
```
A container's environment is fixed when it is created. Apply changes with `docker compose up -d`, which recreates affected services. `docker compose restart` keeps the old environment.
Precedence, highest first:
1. A value stored by the admin dashboard, for the settings listed under [Runtime settings](#runtime-settings-in-the-admin-dashboard).
2. The environment variable.
3. The built-in default.
Use `true` or `false` for booleans, decimal integers for integer settings, and the specified object or array for JSON settings. Defaults and accepted values are listed below. Every service built from this version of the stack files or later reads an empty or blank value the same as an unset one, so an empty value restores the default. Older images do not, which [Match the images to the stack files](/operator/upgrading/#match-the-images-to-the-stack-files) covers.
### Reading a secret from a file
Any setting a Fluxer service reads for its configuration can come from a file instead, such as a Docker secret. Set the variable's name with `_FILE` appended to the file's path, and leave the variable itself empty. The service reads the file at startup and drops one trailing newline. Startup fails, naming both variables, when the variable and its `_FILE` form are both set. It also fails, naming the path, when the file is missing, unreadable or not valid UTF-8. The name to use is the one the service sees, so `POSTGRES_PASSWORD` in `.env` becomes `FLUXER_POSTGRES_PASSWORD_FILE`.
Add the secrets through a local Compose override. `FLUXER_SUDO_MODE_SECRET` is read by `api` and `worker` only:
```yaml
secrets:
sudo_mode_secret:
file: ./secrets/sudo_mode_secret
services:
api:
secrets: [sudo_mode_secret]
environment:
FLUXER_SUDO_MODE_SECRET: ''
FLUXER_SUDO_MODE_SECRET_FILE: /run/secrets/sudo_mode_secret
worker:
secrets: [sudo_mode_secret]
environment:
FLUXER_SUDO_MODE_SECRET: ''
FLUXER_SUDO_MODE_SECRET_FILE: /run/secrets/sudo_mode_secret
```
Compose still checks the required names in `.env` before it applies the override, so keep a placeholder value there. Every Fluxer service gets the same shared settings, so a secret several services read, such as `FLUXER_POSTGRES_PASSWORD`, needs the same two entries on each of them: `api`, `worker`, `gateway`, `media-proxy`, `push`, `admin`, and `snowflakes`, `users`, `gifs`, `messages` and `unfurl` with their `-shard` services. Any service left out keeps the placeholder. The secret file must be readable by the user the container runs as.
The bundled backing containers read their own settings. The `postgres` container takes `POSTGRES_PASSWORD: ''` and `POSTGRES_PASSWORD_FILE` in its own `environment`, and on a fresh volume it creates the database with that password. `seaweedfs-init`, `meilisearch` and `livekit` have no `_FILE` form, so a file-based S3, Meilisearch or LiveKit secret still needs its real value in `.env` for them. `FLUXER_ENV`, `LOG_LEVEL`, `FLUXER_DISABLE_RATE_LIMITS` and the `FLUXER_ERLANG_*` settings other than `FLUXER_ERLANG_COOKIE` are read directly, so set them as plain values.
## Core identity and public address
`FLUXER_DOMAIN` is required. Everything else here is optional.
| Variable | Value in `.env.example` | Controls |
| --- | --- | --- |
| FLUXER_DOMAIN | `chat.example.com` | The hostname users type. Reaches services as `FLUXER_BASE_DOMAIN`. Compose refuses to start when it is unset or empty |
| FLUXER_PUBLIC_SCHEME | `https` | The scheme users see. Must be `http` or `https`. Anything else fails startup |
| FLUXER_PUBLIC_PORT | `443` | The port users see. Integer. Omitted from derived URLs when it is the default for the scheme |
Outside Compose these fall back to an empty base domain, `http`, and port `8088`. Compose supplies `https` and `443`.
The API and worker require hostname-only domain settings: no scheme, port, credentials, path, query, fragment, or whitespace. Bracket IPv6 addresses. Valid hostname spelling is preserved, including case and a terminal root dot.
`FLUXER_DOMAIN` also feeds the default passkey relying party identifier, the default VAPID contact address, and the edge listener address.
`FLUXER_PUBLIC_ORIGIN` states the same public address as one string, and [The public origin](#the-public-origin) has it.
## Secrets you must generate
Every value below ships as `CHANGE_ME`, and the installer generates them all. To set one by hand or rotate it, generate it with `openssl rand -hex 32` unless its entry names another command. Compose refuses to start when one is unset or empty, and names the variable.
#### `POSTGRES_PASSWORD`
The database login, reused as `FLUXER_POSTGRES_PASSWORD`. Requires changing the stored role password too.
#### `MEILI_MASTER_KEY`
The Meilisearch master key, reused as `FLUXER_SEARCH_API_KEY`. Recreate `meilisearch` and every service that searches.
#### `FLUXER_S3_SECRET_KEY`
The object-storage secret, reused as `FLUXER_S3_SECRET_ACCESS_KEY`. `seaweedfs-init` installs it as the object store's only S3 identity on every `compose up`, so a changed value takes effect on the next start.
#### `FLUXER_SUDO_MODE_SECRET`
Sudo mode JWTs, as a raw HS256 key. Changing it ends every active sudo mode session.
#### `FLUXER_CONNECTION_INITIATION_SECRET`
Connection initiation tokens and harvest download links. Changing it invalidates connection initiation tokens not yet verified and harvest download links already issued.
#### `FLUXER_PROFILE_PSEUDONYM_SECRET`
Derives the stable placeholder name and tag that other users see while an account's profile is hidden. Must be byte-identical on `api`, `worker`, and `users-shard`, or the same account shows two different names. Changing it gives every hidden profile a new placeholder. `api` and `worker` refuse to start without it when `FLUXER_ENV` is `production`. `users-shard` refuses to start without it unless `FLUXER_ENV` is `development` or `test`.
#### `FLUXER_GATEWAY_RPC_AUTH_TOKEN`
Internal RPC between the API and the Gateway. Must be byte-identical on `api`, `worker`, and `gateway`.
#### `FLUXER_MEDIA_PROXY_SECRET_KEY`
Media Proxy URLs. Must match across `api`, `worker`, `media-proxy`, `gifs`, and `unfurl`.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64`
Generate with `openssl rand -base64 32`. [Upload relay](/media-proxy/upload-relay/) capability tokens. Must be standard base64 decoding to at least 32 bytes. Must match on `api`, `worker`, and `media-proxy`. `api` and `worker` refuse to start without it in every mode. `media-proxy` refuses to start without it when `FLUXER_MEDIA_PROXY_MODE` is `upload` or `relay`.
#### `FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64`
Generate each entry with `openssl rand -base64 32`. Signs the [attachment URLs](/media-proxy/overview/#signed-attachment-urls) Fluxer returns. A comma-separated list, where each entry must be standard base64 decoding to at least 32 bytes. Spaces around an entry and empty entries are ignored. The first entry signs and every entry verifies. Must match on `api`, `worker`, and `media-proxy`, and must differ from `FLUXER_MEDIA_PROXY_SECRET_KEY`. `api` and `worker` refuse to start when an entry is invalid, and with no entry they return unsigned URLs. `media-proxy` reads it only when `FLUXER_MEDIA_PROXY_MODE` is `mp` or `upload`, and then refuses to start with an invalid entry, or with no entry when `FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE` is `report` or `enforce`.
Compose forwards it from `.env` and passes it empty when unset, so a self-hosted instance returns unsigned URLs until an operator sets one. Setting it alone changes nothing on the read side, because `FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE` stays `off` until it is set too.
Rotate it in three steps, and apply each one with `docker compose up -d` before starting the next:
1. Add the new secret as the second entry. Every `media-proxy` then accepts it before any service signs with it.
2. Move the new secret to the first entry, so it signs.
3. Remove the old secret no sooner than one day after step 2, because an ordinary URL it signed stays valid for up to a day.
A [data package URL](/http-api/messages/#data-package-attachment-urls) inside a harvest export never expires, so removing a secret ends every data package URL it signed at once, including the links inside exports already delivered, and those exports have to be rebuilt. Step 3 is safe for ordinary URLs alone.
#### `FLUXER_ADMIN_SECRET_KEY_BASE`
Admin sessions, CSRF tokens, and OAuth state. Changing it signs every admin out. The admin service refuses to start when it is empty.
#### `FLUXER_ADMIN_OAUTH_CLIENT_SECRET`
The admin OAuth2 client secret. The API requires a non-empty value to serve the admin application.
#### `FLUXER_ERLANG_COOKIE`
The BEAM distribution secret. Only the Gateway reads it. Rotate it on every Gateway node at once when clustering.
#### `LIVEKIT_API_SECRET`
LiveKit access tokens. Must be at least 32 characters.
#### `FLUXER_VAPID_PUBLIC_KEY`
Base64url of the 65-byte uncompressed P-256 point, unpadded, which is 87 characters. Config validation rejects any other shape at boot. `install.sh` derives it with `openssl ecparam` and `openssl ec`, and `npx web-push generate-vapid-keys` produces the same pair.
#### `FLUXER_VAPID_PRIVATE_KEY`
The matching half of the pair. Base64url of the 32-byte P-256 scalar, unpadded, which is 43 characters. The API refuses to start when the scalar does not derive the public point. Both values are required even when nobody uses browser notifications.
The values below ship with a usable value. Both are required.
#### `FLUXER_S3_ACCESS_KEY`
`.env.example` `fluxer`. The object-storage access key. Reaches services as `FLUXER_S3_ACCESS_KEY_ID`, which config validation requires to be non-empty.
#### `LIVEKIT_API_KEY`
`.env.example` `fluxer`. The LiveKit API key. Compose passes it to LiveKit as `LIVEKIT_KEYS` and as the webhook signing key, and to the API as `FLUXER_LIVEKIT_API_KEY`, so one change in `.env` moves them all.
## Endpoint derivation from FLUXER_BASE_DOMAIN
Fluxer builds its public endpoints from the scheme, the base domain, and the port. A port of `443` under `https` or `wss`, or `80` under `http` or `ws`, is omitted.
| Endpoint | Derived value |
| --- | --- |
| api, api_client | scheme, base domain, optional port, `/api` |
| app | scheme, base domain, optional port |
| gateway | `ws` or `wss`, base domain, optional port, `/gateway` |
| media | scheme, base domain, optional port, `/media` |
| static_cdn | scheme, base domain, optional port. With `FLUXER_STATIC_CDN_DOMAIN` set it becomes `https://` and that domain, with no port |
| admin | scheme, base domain, optional port, `/admin` |
| marketing | scheme, base domain, optional port, `/marketing` |
| invite | scheme, `FLUXER_INVITE_DOMAIN` or the base domain, optional port, `/invite` |
| gift | scheme, `FLUXER_GIFT_DOMAIN` or the base domain, optional port, `/gift` |
Nothing in the stack reads `X-Forwarded-Proto` or `X-Forwarded-Host`. Every absolute URL comes from configuration, so set the scheme, the host, and the port by hand and keep them in sync with whatever terminates TLS.
## The public origin
`FLUXER_PUBLIC_ORIGIN` states the public address as one string. It holds the scheme, the host, and the port when that port is not the default for the scheme. Leave it unset unless you need it. Compose otherwise derives every public URL from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT`, non-default ports included.
When set, it must agree with those three. A mismatch can break sign-in, setup, passkeys and media access.
Use an explicit `http://` or `https://` origin. Surrounding spaces and a single trailing slash are accepted. Credentials, paths, query strings, fragments, control characters, and empty or zero ports are rejected.
## Endpoint overrides
Each of these replaces its derived endpoint wholesale. Set one only when part of the instance answers at an address the derivation does not produce. All are optional.
The API and worker validate endpoint URLs at startup. The Gateway endpoint requires `ws://` or `wss://`. The others require `http://` or `https://`. Path prefixes and explicit ports are allowed. Credentials, fragments, whitespace, and backslashes are rejected. HTTP endpoint bases cannot contain a query. The Gateway URL may retain query parameters.
#### `FLUXER_STATIC_CDN_DOMAIN`
Default empty. A separate host for static assets. When set, the static endpoint of `api` and `worker` is forced to `https` with no port. Only `api` and `worker` read it. Set `FLUXER_STATIC_CDN_ENDPOINT` to the same origin as well for `admin`, `app-proxy` and `gateway`.
#### `FLUXER_INVITE_DOMAIN`
Default empty. The host used in invite links. A hostname with no scheme and no path.
#### `FLUXER_GIFT_DOMAIN`
Default empty. The host used in gift links. A hostname with no scheme and no path.
#### `FLUXER_API_ENDPOINT`
Defaults to the derived endpoint. The public API base. Compose forwards it from `.env` to the services on the shared block, which leaves out `app-proxy`. The `admin` service reads the same name as its internal API target, so Compose sets it to `http://api:8080` for that container.
#### `FLUXER_API_CLIENT_ENDPOINT`
Defaults to the derived endpoint. The API base handed to clients. Setting only one of the API endpoints splits what clients see from what the instance advertises.
#### `FLUXER_APP_ENDPOINT`
Defaults to the derived endpoint. The web app origin. Also one of the allowed CORS origins. Serving the client from another hostname requires setting this. Compose forwards it from `.env`, and builds it from the public origin when it is unset.
#### `FLUXER_APP_ORIGIN_ALIASES`
Default empty. Further web app origins, comma separated. Each entry must be an HTTP or HTTPS origin, or the API refuses to start. The API treats each one like the `FLUXER_APP_ENDPOINT` origin. It allows it through CORS, reads invite links on its host, skips unfurling client routes on its host, and refuses webhook token calls from it. Set it when one instance serves the web app on more than one hostname.
#### `FLUXER_GATEWAY_ENDPOINT`
Defaults to the derived endpoint. The public Gateway WebSocket URL.
#### `FLUXER_MEDIA_ENDPOINT`
Defaults to the derived endpoint. The public Media Proxy URL. The `gifs` service accepts it as a fallback for `FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT`. Compose forwards it from `.env`, and builds it from the public origin and `/media` when it is unset.
#### `FLUXER_STATIC_CDN_ENDPOINT`
Defaults to the derived endpoint. The static asset origin. Read by `api`, `worker`, `admin`, and `app-proxy`. When it is unset, `admin` gets the public origin, and every other service derives it itself.
#### `FLUXER_ADMIN_ENDPOINT`
Defaults to the derived endpoint. The admin origin. The API accepts exactly this value plus `/oauth2_callback` as the admin OAuth redirect URI. Compose forwards it from `.env`, and builds it from the public origin and `FLUXER_ADMIN_BASE_PATH` when it is unset.
The admin dashboard sets its cookie flags from the scheme of this value. Over HTTPS its CSRF cookie is named `__Host-csrf_token` and has `Secure`. Over HTTP it is named `csrf_token` without `Secure`.
#### `FLUXER_DOCS_ENDPOINT`
Defaults to an origin built from `FLUXER_PUBLIC_SCHEME`, `FLUXER_BASE_DOMAIN` and `FLUXER_PUBLIC_PORT`. The docs origin. Read by the docs server only, which the stack does not run, so Compose does not forward it.
#### `FLUXER_MARKETING_ENDPOINT`
Defaults to the derived endpoint. The marketing origin. The second allowed CORS origin. Its hostname is extracted and matched. Compose forwards it from `.env`, and uses the public origin with no path when it is unset.
#### `FLUXER_INVITE_ENDPOINT`
Defaults to the derived endpoint. The invite base. The API reads links on its hostname as invite links and does not unfurl them.
#### `FLUXER_GIFT_ENDPOINT`
Defaults to the derived endpoint. The gift base. The API does not unfurl links on its hostname.
Internal endpoints address one container from another and never appear in a browser. `FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT` is required by `gifs`. The rest are optional.
#### `FLUXER_INTERNAL_API_ENDPOINT`
Default `http://127.0.0.1:8080` in the Gateway. The API address used by `gateway` and `push`. `push` refuses to start without it. Compose sets `http://api:8080`.
#### `FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT`
Default `http://127.0.0.1:8082`. The internal Media Proxy address for `api` and `worker`. `FLUXER_MEDIA_PROXY_ENDPOINT` is read as an alias when this name is unset or empty. Compose sets `http://media-proxy:8080`.
#### `FLUXER_MEDIA_PROXY_ENDPOINT`
No default. The internal Media Proxy address. `unfurl-shard` reads this name alone and exits without it. On `api` and `worker` it is an alias of `FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT`, read only when that name is unset or empty. Compose sets `http://media-proxy:8080` on `unfurl-shard` only.
#### `FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT`
No default. The public Media Proxy URL used by `gifs`, `unfurl`, and `media-proxy`. `gifs` requires this or `FLUXER_MEDIA_ENDPOINT` to start. Set it on `media-proxy` so requests for its own assets use local storage instead of an HTTP round trip. For `api` and `worker`, use `FLUXER_MEDIA_ENDPOINT` instead. Compose forwards it from `.env` to every app service. When it is unset, they get `FLUXER_MEDIA_ENDPOINT`, or the public media URL when that is unset too.
## The edge
The `edge` container is the only HTTP entry point. Neither layout below needs a change to the edge settings.
Bundled TLS is the default. `docker compose up -d` binds `80/tcp`, `443/tcp`, and `443/udp` and obtains its own certificate for `FLUXER_DOMAIN`. Point DNS at the host and set nothing else. The `443` publishes stay on `443` whatever `FLUXER_PUBLIC_PORT` says, and `FLUXER_HTTPS_PORT` moves them.
To put your own reverse proxy in front, add the `docker-compose.proxy.yml` overlay. The edge then serves plain HTTP on one port, and the proxy in front terminates TLS. [Behind your own reverse proxy](/operator/reverse-proxy/) has the steps, the requirements, and the per-proxy configuration.
All are optional.
#### `FLUXER_EDGE_SITE_ADDRESS`
Defaults to the address Compose builds from `FLUXER_PUBLIC_SCHEME` and `FLUXER_DOMAIN` with no port. The edge's listen address. With the default, it binds `443` or `80` in the container, and `FLUXER_HTTPS_PORT` or `FLUXER_HTTP_PORT` maps it on the host. Honoured in the bundled layout only. `docker-compose.proxy.yml` sets the literal `:8080` and `tunnel.compose.yml` the literal `:80`, and either discards any `.env` value. A site address that lists more than one hostname therefore needs the bundled layout. Write the scheme into any value you set here. A bare hostname means automatic HTTPS on `443` whatever `FLUXER_PUBLIC_SCHEME` says.
#### `FLUXER_EDGE_TRUSTED_PROXIES`
Default `private_ranges`. The peer addresses whose `X-Forwarded-For` header the edge trusts. [Trusted proxies](/operator/reverse-proxy/#trusted-proxies) lists the ranges and says when to change it.
#### `FLUXER_EDGE_ENCODE`
Default `zstd gzip`. The response compression the edge offers, written as the arguments of the Caddy `encode` directive. `gzip` alone stops the edge offering zstd. Read by the edge container only, and it takes effect on `docker compose up -d edge`.
#### `FLUXER_EDGE_GLOBAL_OPTIONS`
No default. Extra Caddy global options on one line, added to the global block of the edge Caddyfile, such as `email [email protected]` for the ACME account. Compose forwards it from `.env` to `edge`, and an empty value adds nothing.
#### `FLUXER_EDGE_BIND`
Default `127.0.0.1:8080`. Where the plain-HTTP port binds. Read only under the overlay. `0.0.0.0:8080` must be firewalled to the proxy host, because the edge takes the client address from the `X-Forwarded-For` header of any peer in `FLUXER_EDGE_TRUSTED_PROXIES`.
#### `FLUXER_HTTP_PORT`
Default `80`. The host side of the edge's HTTP publish, which serves the redirect to HTTPS and the ACME HTTP challenge under an `https` scheme and nothing at all under an `http` one. The container still listens on `80` inside. It takes an optional bind address in front of the port, so `127.0.0.1:80` keeps the publish off every public interface. Do not set it to the same host port as `FLUXER_HTTPS_PORT`, because that publishes one host port twice and the edge refuses to start. `docker-compose.proxy.yml` ignores it and publishes `FLUXER_EDGE_BIND` instead. `tunnel.compose.yml` reads it with a default of `127.0.0.1:80`.
#### `FLUXER_HTTPS_PORT`
Default `443`. The host side of the edge's HTTPS publish, and the only thing it moves is that host side. Set it when the host already has something on the port the instance advertises. It moves the TCP and the UDP publish together, because HTTP/3 needs both on the same port. It takes the same optional bind address in front of the port. The overlay does not read it.
#### `COMPOSE_FILE`
No default. Which Compose files are loaded. Read by Docker Compose itself. Set it to select the overlay for every command.
`FLUXER_CADDY_SITE_ADDRESS` is the old name for `FLUXER_EDGE_SITE_ADDRESS`. Compose reads it only when `FLUXER_EDGE_SITE_ADDRESS` is unset, so an older `.env` keeps the listener it already had. Rename `FLUXER_CADDY_SITE_ADDRESS` to `FLUXER_EDGE_SITE_ADDRESS` at any time.
## Client IP
[Trusted proxies](/operator/reverse-proxy/#trusted-proxies) explains how the edge picks the client address.
All are optional.
#### `FLUXER_TRUST_CLIENT_IP_HEADER`
Default `false`. Whether the client-IP header is trusted. Compose forwards it from `.env` with a default of `true`, because the edge sets the header on every upstream hop. With `false`, `api` ignores the header and uses the address of the peer that opened the connection. That suits an `api` that clients reach directly. Behind any proxy it makes every request look as though it came from the proxy.
#### `FLUXER_CLIENT_IP_HEADER_NAME`
Default `x-forwarded-for`. Which header has the client IP. Read by `api`, `worker`, `admin`, and `gateway`. It must match what the edge sets.
#### `FLUXER_API_IP_BAN_EXEMPT_IPS`
Default empty. Addresses and CIDR ranges exempt from IP bans. Comma separated. A bare IPv4 address exempts that address, a bare IPv6 address exempts its /64, and a CIDR range such as `2001:db8:1200::/56` exempts every address in it. Every entry must parse as an IP or a CIDR range or the API fails at boot.
While `FLUXER_TRUST_CLIENT_IP_HEADER` is `true`, the API returns 403 for any request whose client-IP header is missing, empty, or not a parsable address. The exceptions are `/_health`, `/webhooks/livekit`, `/test`, and the Bluesky client metadata and JWKS routes. The edge sets the header on every upstream hop, so a missing or unparsable header happens only in a layout that puts something other than the edge directly in front of `api`. A proxy in front of the edge that never sets the header passes the check, and every request then looks as though it came from the proxy.
#### `FLUXER_API_TRUSTED_CALLERS`
Default `[]`, which keys every rate limit on the caller. A JSON array of trusted callers, such as a front end that talks to the API for its own visitors. Each entry is an object with a `name`, a `key` of at least 32 characters, and `buckets`, the rate limit buckets the caller may set the client address for. Only `donation:request_link`, `donation:manage`, `donation:checkout`, `oauth:token` and `oauth:revoke` accept it. Any other bucket name is ignored. A malformed entry or a short key fails the API at boot.
The caller sends its key in `X-Fluxer-Internal-Key` and its own client's address in `X-Fluxer-Client-Ip`. The API keys the request on that address only when the key matches an entry that lists the route's bucket. Otherwise it ignores both headers and keys the request on the caller. Self-hosters leave this empty.
```json
[{"name": "bugs", "key": "<at least 32 characters>", "buckets": ["oauth:token", "oauth:revoke"]}]
```
#### `FLUXER_API_DONATION_PROXY_KEY`
Default empty. The older form of a trusted caller limited to the three donation buckets, for the marketing site. The key must be at least 32 characters or the API fails at boot. The API also accepts the address in the older `X-Fluxer-Donor-Ip` header. Self-hosters leave this empty.
## Images
These pick which container images Compose pulls. All are optional.
| Variable | Value in `.env.example` |
| --- | --- |
| FLUXER_REGISTRY_OWNER | `fluxerapp` |
| FLUXER_REGISTRY | `ghcr.io/${FLUXER_REGISTRY_OWNER}` |
| FLUXER_IMAGE_TAG | `v1` |
`FLUXER_REGISTRY_OWNER` is the owner segment of the image names, `FLUXER_REGISTRY` the registry they are pulled from, and `FLUXER_IMAGE_TAG` the tag on every Fluxer image. Each falls back to the value in the table.
These affect only the Fluxer images. Each bundled third-party service has an image name of its own, which replaces the whole reference, registry and tag included. Set one to pull from a mirror or to move a service to another tag. All are optional.
- `FLUXER_CADDY_IMAGE`: default `caddy:2.11-alpine`, for `edge`.
- `FLUXER_POSTGRES_IMAGE`: default `postgres:16-alpine`, for `postgres`.
- `FLUXER_VALKEY_IMAGE`: default `valkey/valkey:9.1-alpine`, for `valkey`.
- `FLUXER_NATS_IMAGE`: default `nats:2.14-alpine`, for `nats`.
- `FLUXER_MEILISEARCH_IMAGE`: default `getmeili/meilisearch:v1.53`, for `meilisearch`.
- `FLUXER_SEAWEEDFS_IMAGE`: default `chrislusf/seaweedfs:4.47`, for `seaweedfs` and `seaweedfs-init`.
- `FLUXER_LIVEKIT_IMAGE`: default `livekit/livekit-server:v1.12.0`, for `livekit`.
A new Postgres major does not read the data directory an older one wrote. Move `FLUXER_POSTGRES_IMAGE` to another major only through [Move to a new Postgres major version](/operator/upgrading/#move-to-a-new-postgres-major-version). An upgrade refuses a refresh that would change the major the stack runs.
## Database
`FLUXER_POSTGRES_HOST`, `FLUXER_POSTGRES_DATABASE`, `FLUXER_POSTGRES_USERNAME`, and `FLUXER_POSTGRES_PASSWORD` are required in production. The rest are optional.
#### `FLUXER_DATABASE_BACKEND`
Default `postgres`. Which backend is used. Node accepts only `postgres` or `cassandra`. The internal services also accept `postgresql`, `pg`, `scylla`, and `scylladb`. Every service rejects any other value at startup. Compose sets `postgres`.
#### `FLUXER_POSTGRES_URL`
Default empty. A full connection URL. When set, the discrete host, database, user, and password production checks are skipped.
#### `FLUXER_POSTGRES_HOST`
Default `127.0.0.1`. The database host. In production with `postgres` and no URL it must not be `127.0.0.1` or `localhost`. Compose forwards it from `.env` with a default of `postgres`, the bundled service.
#### `FLUXER_POSTGRES_PORT`
Default `5432`. The database port. Integer 1 to 65535.
#### `FLUXER_POSTGRES_DATABASE`
Default `fluxer`. The database name. Must be non-empty. Compose forwards it from `.env`, and passes the same value to the bundled `postgres` as `POSTGRES_DB`. The Postgres image creates that database only on its first start with an empty data directory. Set it before the first start.
#### `FLUXER_POSTGRES_USERNAME`
Default `fluxer`. The role. Must be non-empty. Compose forwards it from `.env`, and passes the same value to the bundled `postgres` as `POSTGRES_USER`, under the same first-start rule.
#### `FLUXER_POSTGRES_PASSWORD`
Default `fluxer`. The password. The literal `fluxer` is rejected in production. `CHANGE_ME` is not. Compose fills it from `POSTGRES_PASSWORD`.
#### `FLUXER_POSTGRES_SSL`
Default `false`. TLS to the database. Must be `true` or `false`. When `false`, an `sslmode` other than `prefer` in `FLUXER_POSTGRES_URL` still applies. In production it must be `true` unless `FLUXER_SELF_HOSTED=true`.
#### `FLUXER_POSTGRES_SSL_CA`
Default empty. A CA certificate in PEM form. Used only when SSL is on.
#### `FLUXER_POSTGRES_MAX_CONNECTIONS`
Default `20`. Pool size. Integer 1 to 1000, per process. The total is the sum across every container. Only `api`, `worker`, `users-shard` and `messages-shard` open a pool, and Compose sets it in each from the per-service name below.
#### `FLUXER_API_POSTGRES_MAX_CONNECTIONS`
Default `25`. The pool size of `api`. Read by Compose, which passes it to `api` as `FLUXER_POSTGRES_MAX_CONNECTIONS`.
#### `FLUXER_WORKER_POSTGRES_MAX_CONNECTIONS`
Default `25`. The pool size of `worker`, passed the same way.
#### `FLUXER_USERS_SHARD_POSTGRES_MAX_CONNECTIONS`
Default `20`. The pool size of `users-shard`, passed the same way.
#### `FLUXER_MESSAGES_SHARD_POSTGRES_MAX_CONNECTIONS`
Default `20`. The pool size of `messages-shard`, passed the same way.
#### `FLUXER_POSTGRES_KV_TABLE`
Default `fluxer_kv`. The key-value table name. Must match a safe Postgres identifier or startup fails.
#### `FLUXER_POSTGRES_PREPARED_STATEMENTS`
Default `true`. Named prepared statements. Must be `true` or `false`. Compose forwards it in the shared block, so one value governs `api`, `worker` and the Rust services at once. Set it to `false` behind a transaction-pooling pooler such as PgBouncer, where a named statement outlives the session that declared it.
Cassandra or Scylla is an alternative backend, selected with `FLUXER_DATABASE_BACKEND=cassandra`. Supply a reachable database and configure the settings below when selecting it. The bundled stack uses Postgres, does not include a Cassandra or Scylla service, and forwards none of the names below.
#### `FLUXER_CASSANDRA_HOSTS`
Default `127.0.0.1`. Contact points. Comma separated. The Rust services normalise each entry to host and port, with IPv6 bracketed.
#### `FLUXER_CASSANDRA_PORT`
Default `9042`. The port. Integer.
#### `FLUXER_CASSANDRA_KEYSPACE`
Default `fluxer`. The keyspace. Must exist already.
#### `FLUXER_CASSANDRA_LOCAL_DC`
Default `datacenter1`. The local datacentre. Read by `api` and `worker` only. The Rust services ignore it.
#### `FLUXER_CASSANDRA_USERNAME`
Default empty. The login. Empty means no authentication.
#### `FLUXER_CASSANDRA_PASSWORD`
Default empty. The password. Empty means no authentication.
## Cache and key-value
The bundled stack requires Valkey for shared cache, live updates and deletion queues. General background jobs use [NATS JetStream](#message-bus-and-internal-services). Change these settings only when customising the cache or connecting an external Redis-compatible service.
#### `FLUXER_KV_URL`
Default `redis://localhost:6379/0`. The Redis-compatible service address. Read by `api` and `worker`. Compose forwards it from `.env` with a default of `redis://valkey:6379/0`, the bundled service.
#### `FLUXER_KV_MODE`
Default `standalone`. Use `standalone` for a single Valkey server or `cluster` for a Redis-compatible cluster.
#### `FLUXER_SVC_CACHE_TTL_MS`
Default `30000`. Cache lifetime in the internal services, in milliseconds. The `users` and `messages` routers ignore it. Their shard caches honour it.
#### `FLUXER_SVC_CACHE_MAX_ENTRIES`
Default `100000`. Cache entry ceiling. Per process. The `users` and `messages` routers ignore it. Their shard caches honour it.
#### `FLUXER_GIFS_SHARD_CACHE_MAX_BYTES`
Default `536870912`. GIF cache size. Must be at least 16777216. Set it to 134217728 to keep the cache inside the `256mb` ceiling `FLUXER_GIFS_SHARD_MEMORY_LIMIT` gives `gifs-shard`.
#### `FLUXER_IP_BAN_REFRESH_INTERVAL_MS`
Default `300000`. How often the API refreshes its IP-ban cache. A non-finite value or one at or below zero, such as `0`, disables the timer.
## Object storage
Every uploaded file lands in an S3-compatible object store, which the stack provides with SeaweedFS. `FLUXER_S3_ACCESS_KEY_ID` and `FLUXER_S3_SECRET_ACCESS_KEY` are required. The rest are optional. [Run a data store outside the stack](/operator/upgrading/#run-a-data-store-outside-the-stack) has the overlay that stops the bundled SeaweedFS once the stack points at another store.
#### `FLUXER_S3_ENDPOINT`
Default `http://localhost:3900`. The S3 API address. `media-proxy` defaults to empty instead. Compose forwards it from `.env` with a default of `http://seaweedfs:8333`, the bundled store.
An endpoint with a bucket in its path, such as `https://s3.example.com/my-bucket` with `FLUXER_S3_FORCE_PATH_STYLE=true`, keeps every Fluxer bucket inside that one bucket. Each bucket name becomes a key prefix there, so uploads land under `fluxer-uploads/` in `my-bucket`.
An endpoint with the bucket in its hostname, such as `https://my-bucket.s3.example.com` with `FLUXER_S3_FORCE_PATH_STYLE=true`, turns each bucket name into a key prefix the same way. The store cannot resolve a Fluxer bucket name as the source of a server-side copy there. The API downloads the object to its temporary directory and uploads the copy from there.
The API cannot list objects through either kind of endpoint, because the store reads the bucket name in a listing request as a key. [Report deletion](/admin-api/reports/#report-retention) lists a report's profile snapshot images first. On a store that answers the listing with an error, the deletion fails. Separate buckets do not have this limit.
#### `FLUXER_S3_PUBLIC_ENDPOINT`
No default. The host substituted into presigned URLs, which are the only place it appears. Compose forwards it from `.env`, and uses `FLUXER_S3_ENDPOINT` when it is unset, except on `app-proxy`, which gets only a value `.env` sets and adds it to its Content Security Policy. `admin` reads it to find the origin that report downloads are presigned against. See `FLUXER_S3_BUCKET_REPORTS`.
#### `FLUXER_S3_FORCE_PATH_STYLE`
Default `false`. Path-style addressing. `media-proxy` defaults to `true` instead. Compose forwards it from `.env` with a default of `true`, which the bundled store needs.
#### `FLUXER_S3_REGION`
Default `local`. The region string. `media-proxy` defaults to `us-east-1`. Compose forwards it from `.env` with a default of `us-east-1`, so every service signs for the same region.
#### `FLUXER_S3_ACCESS_KEY_ID`
Default empty. The access key. Config validation requires it non-empty on `api` and `worker`. Optional for `media-proxy`. Compose fills it from `FLUXER_S3_ACCESS_KEY`.
#### `FLUXER_S3_SECRET_ACCESS_KEY`
Default empty. The secret key. Config validation requires it non-empty on `api` and `worker`. Optional for `media-proxy`. Compose fills it from `FLUXER_S3_SECRET_KEY`.
#### `FLUXER_S3_SESSION_TOKEN`
Default empty. A temporary session token. Read by `media-proxy` only.
#### `FLUXER_S3_BUCKET_CDN`
Default `fluxer`. Processed assets. `media-proxy` defaults to `cdn`. Compose forwards it from `.env` with a default of `fluxer`, so every service and `seaweedfs-init` name the same bucket.
#### `FLUXER_S3_BUCKET_UPLOADS`
Default `fluxer-uploads`. Raw uploads. `media-proxy` defaults to `uploads`, `app-proxy` to `fluxer-uploads`. Compose forwards it from `.env` with a default of `fluxer-uploads` for the same reason.
#### `FLUXER_S3_BUCKET_REPORTS`
Default `fluxer-reports`. Abuse report evidence. Read by `api`, `worker`, and `admin`. Compose forwards it from `.env` with a default of `fluxer-reports`, so every service and `seaweedfs-init` name the same bucket. `admin` allows one image origin for report attachments and profile snapshots in its Content Security Policy, the origin `api` presigns report downloads against. It works that origin out from this name, `FLUXER_S3_PUBLIC_ENDPOINT` or else `FLUXER_S3_ENDPOINT`, `FLUXER_S3_FORCE_PATH_STYLE` and `FLUXER_S3_BUCKET_UPLOADS`, so give `admin` the same values as `api`. The images load only when that origin is an address browsers can reach.
#### `FLUXER_S3_BUCKET_HARVESTS`
Default `fluxer-harvests`. Data archives. Read by `api` and `worker`. Compose forwards it from `.env` with a default of `fluxer-harvests`, so every service and `seaweedfs-init` name the same bucket.
#### `FLUXER_S3_BUCKET_STATIC`
Default `static`. Static assets. Read by `media-proxy` only in `static` mode, which the stack does not use. `api` and `worker` never read it, and `seaweedfs-init` does not create this bucket. Compose does not forward it.
The Media Proxy read path has overrides of its own. All are optional.
#### `FLUXER_S3_READ_ENDPOINT`
Falls back to `FLUXER_S3_ENDPOINT`. The read-side S3 address. Must be http or https with a host, and have no credentials, query string, or fragment.
#### `FLUXER_S3_READ_BUCKET`
Falls back to `FLUXER_S3_BUCKET_CDN`. The read-side bucket. Read by `media-proxy` only.
#### `FLUXER_S3_READ_BUCKET_STYLE`
Defaults to `path` when path style is on, else `virtual`. How the bucket appears in the URL. `path`, `virtual`, or `root`. Anything else is a startup error.
#### `FLUXER_S3_READ_SIGNED`
Default `false`. Whether read requests are signed. Read by `media-proxy` only. Compose forwards it from `.env` with a default of `true`, because `seaweedfs-init` installs an S3 identity and the store then refuses anonymous reads. Set `false` only for a public-read bucket.
## Search
The bundled stack provides Meilisearch for message search. These settings select and connect the search service.
#### `FLUXER_SEARCH_ENGINE`
Default `elasticsearch`. Which engine is used. `elasticsearch` or `meilisearch`. Compose forwards it from `.env` with a default of `meilisearch`, the bundled engine.
#### `FLUXER_SEARCH_URL`
Default `http://127.0.0.1:9200`. The engine address. Compose forwards it from `.env` with a default of `http://meilisearch:7700`.
#### `FLUXER_SEARCH_API_KEY`
Default empty. The API key. Meilisearch uses it as the key. Elasticsearch uses it as an API key that takes precedence over the username and password. Compose fills it from `MEILI_MASTER_KEY`.
#### `FLUXER_SEARCH_USERNAME`
Default empty. Basic auth user. Elasticsearch only. Ignored under Meilisearch.
#### `FLUXER_SEARCH_PASSWORD`
Default empty. Basic auth password. Elasticsearch only.
#### `FLUXER_SEARCH_TLS_REJECT_UNAUTHORIZED`
Default `true`. Certificate verification. Elasticsearch only. Never passed to the Meilisearch client.
## Message bus and internal services
The bundled stack requires NATS for communication between services and JetStream for background jobs. Compose forwards the connection settings from `.env`, with the bundled service as the default. Change them when using an external NATS service, which must have JetStream enabled for the worker connection.
#### `FLUXER_NATS_URL`
Default `nats://127.0.0.1:4222`. The core NATS address for `api`, `worker`, and `gateway`. The Gateway defaults to `nats://nats:4222` instead. Compose forwards it from `.env` with a default of `nats://nats:4222`.
#### `FLUXER_NATS_CORE_URL`
Alias of `FLUXER_NATS_URL` on `api` and `worker`, read only when that name is unset or empty. The core NATS address. Not read by the Gateway. Compose does not forward it, because it forwards `FLUXER_NATS_URL`.
#### `FLUXER_NATS_JETSTREAM_URL`
Default `nats://127.0.0.1:4222`. The JetStream address for `api` and `worker`. Compose forwards it from `.env`, and uses `FLUXER_NATS_URL` when it is unset.
#### `FLUXER_NATS_AUTH_TOKEN`
Default empty. NATS authentication. Read by `api`, `worker`, `gateway`, and the internal services. The shipped NATS runs without authentication, and Compose forwards this name to every container that connects to it.
#### `FLUXER_SVC_NATS_URL`
Default `nats://127.0.0.1:4222`. The NATS address for the internal services and `push`. A separate variable from `FLUXER_NATS_URL`. Compose forwards it from `.env`, and uses `FLUXER_NATS_URL` when it is unset.
#### `FLUXER_GATEWAY_API_RPC_ENDPOINT`
No default. Where the Gateway calls the API. Read by the Gateway, which derives it from `FLUXER_INTERNAL_API_ENDPOINT` when it is unset. Compose does not forward it.
Compose configures the internal services. Change the following settings only when customising their deployment.
#### `FLUXER_SVC_NAME`
Default `default`. The metrics prefix. It also selects the built-in default of `FLUXER_SVC_MAX_CONCURRENT_REQUESTS`. Changing it does not rename NATS subjects. Compose sets it per service.
#### `FLUXER_SVC_MODE`
Default `router`. Must be `router` or `shard`. Any other value prevents startup. Compose sets it per service.
#### `FLUXER_SVC_SHARD_COUNT`
Default `1`. How many shards exist. Compose fixes it at `1`.
#### `FLUXER_SVC_SHARD_ID`
Derived from the numeric suffix of `POD_NAME`, else `0`. Which shard this process is. A shard ID at or above the shard count is a startup error. Compose sets `0` on every shard.
#### `FLUXER_SVC_LISTEN_HOST`
Default `0.0.0.0`. The bind address. Serves health and metrics only. Compose does not forward it.
#### `FLUXER_SVC_PORT`
Default `8090`. The health and metrics port. Not published. Compose does not forward it, because every internal service health check probes `8090`.
#### `FLUXER_SVC_MAX_CONCURRENT_REQUESTS`
Defaults to 192 for messages, 320 for snowflakes, 64 otherwise. In-flight request ceiling. The built-in defaults key off `FLUXER_SVC_NAME`. Compose forwards it from `.env` to every internal service router and shard. Left unset, each keeps its built-in default. Once set, the one value replaces the default on all of them, so a value below 320 also lowers the snowflakes ceiling. A router holds a slot for the whole round trip to its shard. A request over the ceiling is rejected at once. The API request behind it fails with a 503, and the API logs `shard rejected the request because it is at its concurrency limit`.
#### `POD_NAME`
No default. The shard ordinal source and node identity. Also read by the Gateway, and by the API as the `pod_name` in its RPC timing records. Compose does not set it.
The API reaches the internal services over NATS. Compose forwards the client names and the tuning values below from `.env`. It does not forward `FLUXER_SNOWFLAKE_SERVICE_SUBJECT`, `FLUXER_USERS_SERVICE_SUBJECT` or `FLUXER_GIF_SERVICE_SUBJECT`, because each has to match the subject its internal service listens on.
Numeric settings require decimal safe integers. Surrounding whitespace is trimmed, and omitted or blank settings use the defaults. Explicit malformed or out-of-range values are rejected.
- `FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME`, `FLUXER_USERS_SERVICE_NATS_CLIENT_NAME` and `FLUXER_GIF_SERVICE_NATS_CLIENT_NAME`: the name the API gives each NATS connection, as NATS monitoring shows it. Each defaults to a name for its service.
- `FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE`: defaults to `128`. Accepts 1 to 512.
- `FLUXER_SNOWFLAKE_SERVICE_LOW_WATERMARK`: defaults to `32`, capped below the batch size. Accepts 0 through batch size minus one.
- `FLUXER_SNOWFLAKE_SERVICE_MAX_BUFFER_AGE_MS`: defaults to `5000`. Accepts 1 to 60000 milliseconds.
- `FLUXER_SNOWFLAKE_SERVICE_REQUEST_TIMEOUT_MS`: defaults to `6000`. Accepts 1 to 60000 milliseconds.
- `FLUXER_USERS_SERVICE_TIMEOUT_MS`: defaults to `6000`. Accepts 1 to 2147483647 milliseconds.
- `FLUXER_USERS_SERVICE_INFLIGHT_MAX_ENTRIES`: defaults to `10000`. Accepts 0 through the largest safe integer.
- `FLUXER_GIF_SERVICE_TIMEOUT_MS`: defaults to `12000`. Accepts 1 to 2147483647 milliseconds.
- `FLUXER_GIF_SERVICE_REGISTER_SHARE_TIMEOUT_MS`: defaults to `3000`. Accepts 1 to 2147483647 milliseconds.
A Snowflake low watermark of `0` disables background refill before the buffer is empty. A Users in-flight entry limit of `0` disables request coalescing. It is not a limit on concurrent requests.
## Voice and LiveKit
LiveKit is the media server for voice and video. `FLUXER_LIVEKIT_API_KEY` and `FLUXER_LIVEKIT_API_SECRET` are required for voice. The rest are optional.
#### `FLUXER_LIVEKIT_ENABLED`
Default `false`. The master switch for all voice and video. Compose forwards it from `.env` with a default of `true`, because the stack ships LiveKit.
#### `FLUXER_LIVEKIT_API_KEY`
Default empty. The LiveKit API key. Compose fills it from `LIVEKIT_API_KEY`.
#### `FLUXER_LIVEKIT_API_SECRET`
Default empty. The LiveKit secret. Compose fills it from `LIVEKIT_API_SECRET`.
#### `FLUXER_LIVEKIT_URL`
Default empty. The client-facing LiveKit URL. Set it only when LiveKit is served from another host. `docker-compose.yml` never passes it empty: it builds the value from `FLUXER_PUBLIC_ORIGIN`, or from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN` and `FLUXER_PUBLIC_PORT` when the origin is unset, followed by `/livekit`, and forwards a value `.env` sets. An empty value outside Compose has the API derive `wss://host/livekit` from the public API origin, or `ws://` under `http`, with a non-default port kept.
#### `FLUXER_LIVEKIT_USE_EXTERNAL_IP`
Default `true`. Whether LiveKit discovers the address browsers dial by asking a STUN server. A host that cannot reach one over UDP fails to start with `could not resolve external IP: context deadline exceeded`. Set it to `false` and give `FLUXER_LIVEKIT_NODE_IP` the address instead. Read only by the `livekit` service.
#### `FLUXER_LIVEKIT_NODE_IP`
Default empty. The address LiveKit publishes in its ICE candidates. Empty leaves the choice to the discovery above, and with that discovery off LiveKit falls back to the container's own address, which no browser can reach. Set both together. `docker compose logs livekit` names the address in use as `nodeIP` on the starting line.
#### `FLUXER_LIVEKIT_STUN_PRIMARY`
Default `stun.l.google.com:19302`. The first STUN server the discovery asks. Point it at another server to keep the discovery without reaching Google.
#### `FLUXER_LIVEKIT_STUN_SECONDARY`
Default `stun1.l.google.com:19302`. The second STUN server, used the same way.
#### `FLUXER_LIVEKIT_INTERNAL_URL`
Default empty. The server-side LiveKit control API. Compose forwards it from `.env` with a default of `http://livekit:7880`, the bundled service.
#### `FLUXER_LIVEKIT_DEFAULT_REGION`
No default. The default voice region. JSON with `id`, `name`, `emoji`, `latitude`, and `longitude`. Compose forwards it from `.env`, with a default region named `Default` at latitude and longitude `0`.
#### `FLUXER_LIVEKIT_LOG_LEVEL`
Default `info`. How much LiveKit logs. Read by Compose only, which writes it into the LiveKit configuration as `log_level`.
#### `FLUXER_LIVEKIT_TCP_PORT`
Default `7881`. The TCP media port. Read by Compose only. Compose publishes it on the host and passes it to LiveKit as `rtc.tcp_port`, so the port LiveKit advertises in ICE candidates is the one the host forwards.
#### `FLUXER_LIVEKIT_UDP_PORT`
Default `7882`. The UDP media port, published and passed to LiveKit as `rtc.udp_port` the same way.
Compose points LiveKit at the webhook target `http://api:8080/webhooks/livekit` and configures no TURN server. The API accepts requests on that route without user authentication and without a client-IP header. A client that cannot use UDP falls back to ICE-TCP on the TCP port.
Moving a media port is one line in `.env` followed by `docker compose up -d livekit`. Compose puts the same value on the host side of the mapping, on the container side, and on the `rtc` port LiveKit advertises in the ICE candidates it hands to clients.
To rotate the key pair, set both names in `.env`, then run `docker compose up -d livekit api worker` to apply the change everywhere.
### Peer-to-peer calls
A peer-to-peer call sends media directly between its participants and not through LiveKit or any relay. It runs only while the `voice_p2p` experiment is enabled in the [instance configuration](/admin-api/instance/). The value below names the STUN servers Fluxer hands to each participant. It is optional. The stack ships no STUN server.
#### `FLUXER_VOICE_P2P_STUN_URLS`
Default empty. A comma-separated list of `stun:` URLs. Participants use them to discover their public addresses.
Unset, participants receive no ICE servers and connect only when they can already reach each other, as on one local network. Two participants that cannot reach each other directly fail to connect, and either one can switch the call to LiveKit. Participants in a peer-to-peer call can learn each other's IP addresses.
## Email
Only an instance where members sign in with email sends mail. A username instance sends none whatever the values below hold, and needs none of them. [Sign-in methods](/operator/sign-in/) covers the difference.
Email is off by default. It works only when the email switch is on and the provider is `smtp` with `FLUXER_EMAIL_FROM_EMAIL`, `FLUXER_EMAIL_SMTP_HOST`, `FLUXER_EMAIL_SMTP_PORT`, `FLUXER_EMAIL_SMTP_USERNAME` and `FLUXER_EMAIL_SMTP_PASSWORD` all non-empty. The switch is the value saved in the admin dashboard, or `FLUXER_EMAIL_ENABLED` when the dashboard has no saved value. An SMTP value saved in the admin dashboard also wins over the matching variable. All are optional.
While email is on, registration checks the address domain. It has to publish an `MX` record, or an `A` or `AAAA` record as a fallback. An address at a domain that publishes neither is answered `That email domain cannot receive mail.` however well formed it is. The first admin account is no exception. An owner address at a `.lan`, `.internal` or `home.arpa` name needs email left off.
#### `FLUXER_EMAIL_ENABLED`
`.env.example` `false`. The delivery switch. The admin dashboard value wins over this.
#### `FLUXER_EMAIL_PROVIDER`
`.env.example` `none`. The transport. `smtp` or `none`. Anything else fails startup.
#### `FLUXER_EMAIL_FROM_EMAIL`
`.env.example` `[email protected]`. The sender address. Default empty. Compose forwards it from `.env`, and uses `noreply@localhost` when it is unset.
#### `FLUXER_EMAIL_FROM_NAME`
`.env.example` empty. The sender name. Default empty. An empty value sends mail under the instance product name, which is the name saved in the admin dashboard or else `FLUXER_APP_PRODUCT_NAME`, with the [self-hosted host name rule](#instance-identity-and-branding) applied, and a rename in the dashboard applies to the next email. A set value always wins.
#### `FLUXER_EMAIL_REPLY_TO_EMAIL`
Default empty. The address replies go to, sent as the `Reply-To` header on every email. An empty value sends no `Reply-To` header. Must be a single bare address such as `[email protected]`, or the API fails at boot. The admin dashboard has no field for it. `.env.example` lists it commented out.
#### `FLUXER_EMAIL_APP_BASE_URL`
`.env.example` empty. The base URL used in links. Must be http or https with no username, password, query, or fragment, or the API fails at boot. Falls back to the app endpoint.
#### `FLUXER_EMAIL_SMTP_HOST`
`.env.example` empty. The SMTP host. Setting any SMTP variable creates the whole SMTP block, which is absent by default.
#### `FLUXER_EMAIL_SMTP_PORT`
`.env.example` `587`. The SMTP port. Falls back to 587.
#### `FLUXER_EMAIL_SMTP_USERNAME`
`.env.example` empty. The SMTP login. Part of the completeness check.
#### `FLUXER_EMAIL_SMTP_PASSWORD`
`.env.example` empty. The SMTP password. Part of the completeness check.
#### `FLUXER_EMAIL_SMTP_TLS_MODE`
`.env.example` empty. How the SMTP connection is encrypted. Anything other than the four values below fails startup.
| Value | Connection |
| ----- | ---------- |
| `implicit` | TLS from the first byte. Most providers serve this on port 465 |
| `starttls` | A plain connection that must upgrade with STARTTLS before the login. Most providers serve this on port 587 |
| `opportunistic` | A plain connection that upgrades with STARTTLS when the server offers it, and stays plain when it does not |
| `none` | No TLS, even when the server offers STARTTLS. Only for a mail server on a network you trust |
An empty value picks the mode from the port: `implicit` on port 465 and `starttls` on every other port. A mode saved in the admin dashboard wins over this variable, and choosing Automatic there hands the choice back to it. The dashboard shows the mode outgoing email ends up with. Automatic in the setup wizard saves the mode the port calls for, so it never follows this variable or `FLUXER_EMAIL_SMTP_SECURE`.
#### `FLUXER_EMAIL_SMTP_SECURE`
`.env.example` unset. The older switch, read only while `FLUXER_EMAIL_SMTP_TLS_MODE` is empty. `true` means `implicit` and `false` means `opportunistic`. `.env.example` lists it commented out. An older `.env` can hold `FLUXER_EMAIL_SMTP_SECURE=true` next to port 587, and that pair fails against a STARTTLS server with `wrong version number`. Remove the line or set `FLUXER_EMAIL_SMTP_TLS_MODE`.
The Save and test button in the admin dashboard, and [Create SMTP test](/admin-api/instance/#create-smtp-test) with an empty body, connect with the saved settings, the same ones outgoing email uses, and fail with the reason when outgoing email is turned off or incomplete. A send that fails is logged by `api` or `worker` as `SMTP send failed` with the host, port and TLS mode.
The API reads `FLUXER_EMAIL_WEBHOOK_SECRET` for inbound delivery webhooks. `.env.example` lists it commented out.
The API registers `POST /webhooks/sweego` on every deployment. The route authenticates the Standard Webhooks header triple `webhook-id`, `webhook-timestamp`, and `webhook-signature` against that secret, and answers 404 `Email not enabled` while email is off. The route sits outside the client-IP exempt list that covers `/webhooks/livekit`, so give the provider a delivery URL that goes through the edge, `https://chat.example.com/api/webhooks/sweego`.
A second SMTP provider can take the mail for chosen recipient domains while everything else keeps going through the one above. It is off by default. Setting any fallback variable turns it on, and from then on the domains, host, username and password are all required, or startup fails with the name of the one that is missing. The sender, `Reply-To` header and bounced-address check are the same for both providers. Email still has to be on with the main SMTP provider complete, because the fallback never sends on its own. The admin dashboard has no fields for it, and the SMTP test only checks the main provider. Bounce webhooks come only from the provider behind `/webhooks/sweego`, so a hard bounce at the fallback provider does not mark the address as bounced.
#### `FLUXER_EMAIL_FALLBACK_DOMAINS`
Default empty. The recipient domains that go through the fallback provider, comma separated, such as `gmail.com,googlemail.com`. Matching ignores case and takes the part of the address after the last `@`. Only an exact match counts, so `gmail.com` leaves `mail.gmail.com` on the main provider. Set it to `*` alone to send every email through the fallback provider. An entry that is not a bare domain or `*` fails startup. `.env.example` lists it commented out.
#### `FLUXER_EMAIL_FALLBACK_SMTP_HOST`
Default empty. The fallback SMTP host. Required when the domains are set.
#### `FLUXER_EMAIL_FALLBACK_SMTP_PORT`
Default `587`. The fallback SMTP port.
#### `FLUXER_EMAIL_FALLBACK_SMTP_USERNAME`
Default empty. The fallback SMTP login. Required when the domains are set.
#### `FLUXER_EMAIL_FALLBACK_SMTP_PASSWORD`
Default empty. The fallback SMTP password. Required when the domains are set.
#### `FLUXER_EMAIL_FALLBACK_SMTP_TLS_MODE`
Default empty. How the fallback connection is encrypted, with the same four values as `FLUXER_EMAIL_SMTP_TLS_MODE`. An empty value picks `implicit` on port 465 and `starttls` on every other port. A failed send through the fallback is logged as `SMTP send failed` with the fallback host.
#### `FLUXER_EMAIL_FALLBACK_SMTP_HEADERS`
Default empty. Extra headers added to every message sent through the fallback provider, as a JSON object of header name to string value, such as `{"X-Example-Tracking":"off"}` for a provider that reads a header to turn off link tracking. Values are sent exactly as given, never encoded or folded. A name must be a valid header field name, a value must be printable ASCII without line breaks, and each header has to fit on one 998 character line. `From`, `To`, `Cc`, `Bcc`, `Subject`, `Reply-To`, `Message-ID`, `Date`, `Content-Type` and `MIME-Version` cannot be set here. Anything else fails startup with the reason. `.env.example` lists it commented out.
Only `api` and `worker` build the mail service, so recreating those two is enough after a change.
## CAPTCHA
[ALTCHA](/topics/captcha/) proof-of-work is on by default. Turn it off or tune it under Instance Config, Bot protection in the admin panel. No environment variables configure it.
## Single sign-on and passkeys
All are optional.
#### `FLUXER_SSO_ALLOW_PRIVATE_ADDRESSES`
Default `false`. Whether the SSO provider URL may resolve to a private address. Off by default as SSRF protection. Turn it on only for split-horizon DNS or a LAN identity provider.
#### `FLUXER_PASSKEY_RP_ID`
Defaults to the host of `FLUXER_PUBLIC_ORIGIN` when that is set, and to `FLUXER_BASE_DOMAIN` otherwise. The WebAuthn relying party identifier. Compose forwards it from `.env` with a default of `FLUXER_DOMAIN`. With that default, moving the public origin to another host leaves existing passkeys valid.
#### `FLUXER_PASSKEY_RP_NAME`
Default `Fluxer`. The relying party name browsers display. Does not follow `FLUXER_DOMAIN`.
#### `FLUXER_PASSKEY_ADDITIONAL_ALLOWED_ORIGINS`
The complete accepted origin set, comma separated. Despite the name, an explicit value replaces the defaults and must list every origin clients use. Compose forwards it from `.env` with a default of the public web app origin, so a self-hosted instance accepts its own origin alone. When the variable is omitted or empty outside Compose, the API accepts the configured app origin alongside the built-in Fluxer web and Android origins. To accept only the app origin, list it alone.
Each web entry must be an HTTP(S) origin with no credentials, path beyond an optional final slash, query, or fragment. Web origins are normalised to browser form using the configured public port unless the entry supplies one. Android entries must use `android:apk-key-hash:` followed by the signing certificate's SHA-256 fingerprint in canonical, unpadded base64url (43 characters). Invalid entries and control characters fail startup.
:::caution[Changing the relying party identifier invalidates every passkey]
A passkey is bound to the `FLUXER_PASSKEY_RP_ID` it was registered under. Members have to enrol again after a change, so pick the value before opening registration.
:::
## Bluesky connections
Bluesky connections are off by default. Enable them in the admin dashboard's Runtime Integrations panel and supply an ES256 private signing key with a unique key identifier. The public API must serve the [client metadata and signing keys](/http-api/connections/#get-bluesky-client-metadata).
For environment-based configuration, use `FLUXER_AUTH_BLUESKY_ENABLED`, `FLUXER_AUTH_BLUESKY_KEYS`, and the optional `FLUXER_AUTH_BLUESKY_CLIENT_NAME`, `FLUXER_AUTH_BLUESKY_CLIENT_URI`, `FLUXER_AUTH_BLUESKY_LOGO_URI`, `FLUXER_AUTH_BLUESKY_TOS_URI`, and `FLUXER_AUTH_BLUESKY_POLICY_URI` settings.
## Web push
`FLUXER_VAPID_PUBLIC_KEY` and `FLUXER_VAPID_PRIVATE_KEY` are required. `FLUXER_VAPID_EMAIL` is optional.
| Variable | Value in `.env.example` | Controls |
| --- | --- | --- |
| FLUXER_VAPID_PUBLIC_KEY | `CHANGE_ME` | The VAPID public key. Base64url of the 65-byte uncompressed P-256 point |
| FLUXER_VAPID_PRIVATE_KEY | `CHANGE_ME` | The VAPID private key. Base64url of the 32-byte scalar, and the matching half of the pair |
| FLUXER_VAPID_EMAIL | unset | The VAPID contact address. Compose forwards it from `.env`, with a default of `admin@` followed by `FLUXER_DOMAIN` |
`FLUXER_GATEWAY_PUSH_ENABLED` is read by the Gateway alone and defaults to `true`. When it is `false`, the Gateway hands no message or clear notification to `push`.
## Mobile push
`push` reads every name below. `api` also reads the APNs names, apart from `FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT`. Compose forwards the APNs names from `.env` to both, and `FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT` and the FCM names to `push` alone. All are optional.
#### `FLUXER_PUSH_APNS_ENABLED`
Default `false`. The APNs switch. Read by `api` and `push`.
#### `FLUXER_PUSH_APNS_TEAM_ID`
No default. The Apple team. Paired with the key ID.
#### `FLUXER_PUSH_APNS_KEY_ID`
No default. The signing key ID. Paired with the team ID.
#### `FLUXER_PUSH_APNS_PRIVATE_KEY`
No default. The signing key in PEM form. Set exactly one of this and the path.
#### `FLUXER_PUSH_APNS_PRIVATE_KEY_PATH`
No default. A path to the signing key. Must be readable inside the container.
#### `FLUXER_PUSH_APNS_DEFAULT_ENVIRONMENT`
Default `production`. Which APNs environment is used. `production` or `development`. Read by `push` only.
#### `FLUXER_PUSH_APNS_APPS`
Default `[]`. Per-app APNs configuration. JSON array. An entry with no `app_id` fails startup.
#### `FLUXER_PUSH_FCM_ENABLED`
Default `false`. The FCM switch. Read by `push` only, like every FCM name.
#### `FLUXER_PUSH_FCM_PROJECT_ID`
No default. The Firebase project. Paired with the client email.
#### `FLUXER_PUSH_FCM_CLIENT_EMAIL`
No default. The service account address. Paired with the project.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY`
No default. The service account key. Set one of this, `FLUXER_PUSH_FCM_PRIVATE_KEY_PATH`, or `FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH`.
#### `FLUXER_PUSH_FCM_PRIVATE_KEY_PATH`
No default. A path to the key. Must be readable inside the container.
#### `FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH`
No default. A path to the whole service account JSON. Must be readable inside the container.
#### `FLUXER_PUSH_FCM_TOKEN_URI`
Default `https://oauth2.googleapis.com/token`. The OAuth token endpoint. Change only for a proxy or a test double.
#### `FLUXER_PUSH_FCM_APPS`
Default `[]`. Per-app FCM configuration. JSON array, under the same `app_id` rule as APNs.
## Push service settings
`push` is the push notification service in the bundled stack. It reads the VAPID pair from [Web push](#web-push), the APNs and FCM names from [Mobile push](#mobile-push), and `FLUXER_SVC_NATS_URL` with `FLUXER_NATS_AUTH_TOKEN` from [Message bus and internal services](#message-bus-and-internal-services). It refuses to start without `FLUXER_INTERNAL_API_ENDPOINT` and `FLUXER_GATEWAY_RPC_AUTH_TOKEN`. Compose supplies both. The names below belong to `push`, and Compose forwards every one from `.env` except the bind address and port. All are optional.
#### `FLUXER_PUSH_SERVICE_HOST`
Default `0.0.0.0`. The bind address. It must parse as an IP address. Also settable with `--bind-host`. Compose does not set it, so the default applies.
#### `FLUXER_PUSH_SERVICE_PORT`
Default `8126`. The listen port for the health and metrics endpoints. Also settable with `--port`. Compose does not set it, so the default applies.
#### `FLUXER_PUSH_SERVICE_QUEUE_CAPACITY`
Default `10000`. Notification jobs `push` holds at once. Accepts 1 to 1000000.
#### `FLUXER_PUSH_SERVICE_SEND_CONCURRENCY`
Default `256`. Provider requests in flight at once, across every job. Accepts 1 to 65536.
#### `FLUXER_PUSH_SERVICE_APNS_BASE_URL`
No default. Replaces the APNs host in both environments. Set it only for a test double.
#### `FLUXER_PUSH_SERVICE_FCM_BASE_URL`
Default `https://fcm.googleapis.com`. The FCM host. Set it only for a proxy or a test double.
#### `FLUXER_PUSH_SERVICE_RELAY_CONSENT_ACCEPTED`
Default `false`. Accepts the push relay supplemental privacy notice for this `push` process. `true`, `1`, or `yes` accepts it, and `false`, `0`, or `no` leaves the decision to the [push relay configuration](/admin-api/instance/#push-relay-configuration-object). Any other value fails startup. When it is `true`, `push` sends through the Fluxer-run relay even while the stored consent is off.
#### `FLUXER_PUSH_SERVICE_MANAGED_RELAY_HOSTS`
Default `push.fluxer.com`. Hostnames, comma separated, of the Fluxer-run push relay. A browser push endpoint on one of them needs the relay consent above, and a refusal from it counts against the relay quota.
#### `FLUXER_PUSH_SERVICE_OWN_RELAY_HOSTS`
Default empty. Hostnames, comma separated, of a push relay this instance runs itself. `push` delivers a browser push endpoint on one of them straight through its own APNs or FCM credentials, with no HTTP hop.
#### `FLUXER_PUSH_SERVICE_PRIVATE_HOSTS`
Default empty. Hostnames, comma separated, that a push endpoint may use even when they resolve to private, loopback, or other non-public addresses, such as a ntfy server on your own network. `push` refuses every other such endpoint and logs `blocked_address`.
`push` also runs as a relay under `--mode relay`, which the stack does not use. Only that mode reads `FLUXER_PUSH_RELAY_MAX_CONCURRENT`, default `1024`, `FLUXER_PUSH_RELAY_MAX_BODY_BYTES`, default `2816`, `FLUXER_PUSH_RELAY_TRUSTED_PROXY_HOPS`, default `1`, and `FLUXER_PUSH_RELAY_SOURCE_BUCKET_ENABLED`, default `false`. Compose does not forward them, and [Names the stack has no use for](#names-the-stack-has-no-use-for) lists them.
## Payments
Stripe billing for a paid premium tier and gift purchases. All are optional. On a self-hosted instance, set billing in the admin dashboard's [Premium & billing](#runtime-settings-in-the-admin-dashboard) group instead. A value stored there wins over the matching variable below.
A self-hosted instance sells premium only while its premium mode is `mirror`, billing is switched on, a Stripe secret key is set, and at least one currency has both a monthly and a yearly price. Instance discovery reports this as `stripe_enabled`. Existing subscribers can still manage, cancel and renew while the premium mode is `mirror` and a Stripe secret key is set, even after sales are switched off or the prices are removed. Instance discovery reports this as `stripe_serviceable`. Stripe sends events to `https://<FLUXER_DOMAIN>/api/stripe/webhook`, which the dashboard shows as the webhook URL to register in Stripe.
#### `FLUXER_STRIPE_ENABLED`
Default `false`. The Stripe switch.
#### `FLUXER_STRIPE_SECRET_KEY`
Default empty. The Stripe secret key.
#### `FLUXER_STRIPE_WEBHOOK_SECRET`
Default empty. The webhook signing secret.
#### `FLUXER_STRIPE_PRICES`
Default `{}`. Every price ID at once. JSON object.
Individual price variables also exist, one per product and currency: `FLUXER_STRIPE_PRICE_MONTHLY_`, `FLUXER_STRIPE_PRICE_YEARLY_`, `FLUXER_STRIPE_PRICE_GIFT_1_MONTH_` and `FLUXER_STRIPE_PRICE_GIFT_1_YEAR_` in USD, EUR, BRL, DKK, INR, ISK, NOK, PLN, SEK, and TRY. Each overrides the matching price in this object. Compose does not forward them, so a self-hosted instance sets prices with this variable or in the admin dashboard.
Prices saved in the dashboard replace this catalogue entirely. They take any three-letter currency code. A buyer gets the currency mapped to their country, then the default currency, then the first configured one. Fluxer's country checks for localised currencies apply only to prices set by these variables.
A self-hosted instance never sets Stripe Adaptive Pricing on its Checkout Sessions, so the setting in the Stripe Dashboard applies. The hosted service turns it off for SEK, NOK, DKK and ISK sessions so buyers pay the native price.
#### `FLUXER_STRIPE_LEGACY_PRICES`
Default `{}`. Retired price IDs, keyed by the same slot names `FLUXER_STRIPE_PRICES` uses, each mapped to a list, such as `{"monthly_brl": ["price_..."]}`. JSON object. Existing subscriptions on these prices keep renewing. The localised checkout catalogue uses `FLUXER_STRIPE_PRICES` alone. Legacy prices saved in the dashboard replace this variable, and there the currency in a slot name is upper case, such as `monthly_GBP`.
Reprice a slot in this order. Create the new price in Stripe, move the ID it replaces into this variable, roll the API, and only then point `FLUXER_STRIPE_PRICES` at the new price. A price ID that neither variable names is unknown to the API. A renewal invoice on it fails the webhook with `Unknown product for invoice renewal`, a checkout completing on it fails with `Unknown price ID for checkout session`, and both keep failing until the ID is registered. The API answers Stripe once the signature verifies and hands the event to `worker`. Retries come from `worker` rerunning the `processStripeWebhook` job. The Stripe dashboard shows the delivery as succeeded, and the error is in the `worker` logs. Keep a retired ID listed while any subscription still bills on it. For a yearly price that is at least a year after the switch.
With prices saved in the dashboard, add the retired ID to its legacy prices before you replace the price.
## Moderation and abuse
All are optional.
#### `FLUXER_CLAMAV_ENABLED`
Default `false`. Upload virus scanning. No ClamAV container ships.
#### `FLUXER_CLAMAV_HOST`
Default `127.0.0.1`. The scanner host. Needs a reachable scanner.
#### `FLUXER_CLAMAV_PORT`
Default `3310`. The scanner port. Integer.
#### `FLUXER_CLAMAV_FAIL_OPEN`
Default `false`. Behaviour when the scanner is unreachable. With scanning on and this off, an unreachable scanner rejects every upload.
#### `FLUXER_BLOCKLIST_FEEDS_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Off by default on a self-hosted instance. With feeds on, the worker downloads the URLhaus and PhishTank URL lists every six hours, and MalwareBazaar file hashes every twelve hours. Fluxer checks posted links against the URLs and uploads against the hashes.
With feeds off, Fluxer checks none of this data. The first worker start with feeds off removes the URL feed file and every `malware_bazaar` file-SHA ban that no Admin added. File-SHA bans added through the Admin API stay. Turning feeds back on downloads the URLs within six hours and the hashes within twelve.
#### `FLUXER_REPORT_RETENTION_DAYS`
Default `365`. Read by `worker`. The daily [report retention](/admin-api/reports/#report-retention) job deletes each resolved report this many days after it was resolved, with its evidence copies and search entry. Pending reports are kept until they are resolved. A legal hold, a `child_safety` category or a linked enforcement decision can keep a report longer. Integer 1 to 36500, checked at startup.
#### `FLUXER_REPORT_RETENTION_DRY_RUN`
Default `false`. Read by `worker`. With `true`, each run only counts what it would delete or clear and changes nothing. `true` or `false`, checked at startup. Deleted evidence cannot be restored. Check the counts in the `Processed report retention` log line before the first real run.
#### `FLUXER_BREACHED_PASSWORD_CHECK_ENABLED`
Defaults to the inverse of `FLUXER_SELF_HOSTED`. Breached password rejection. Sends the first five characters of the password's SHA-1 hash to `api.pwnedpasswords.com`. Off by default on a self-hosted instance.
## Stored instance policy
Use the [admin dashboard](#runtime-settings-in-the-admin-dashboard) or [Admin instance API](/admin-api/instance/) to change saved settings. Missing settings use their documented defaults. Invalid JSON, field types, identifiers or out-of-range values cause an error instead of silently resetting security or registration policy.
Invalid saved configuration prevents the API and worker from starting. If a running process cannot apply an update, it logs the error and keeps its previous valid settings. Check the reported section and field paths, repair the saved configuration, then retry. It is not repaired automatically.
The API and worker read configuration saved by older releases under these rules:
- Gateway rollout accepts `nats_request_timeout_ms` only when `rpc_request_timeout_ms` is absent. Both require an integer from 1000 to 60000. Use the current name in Admin requests.
- Registration accepts `adminRegistrationUrlsEnabled` only when `admin_registration_urls_enabled` is absent. Missing registration settings default to `open` with admin registration URLs enabled.
- Missing SSO settings default to disabled. A missing enforcement flag takes the value of the enabled flag, and automatic provisioning defaults to on. Stored flags require `true` or `false`.
- SSO allowed domains accept a JSON string array or a legacy comma-separated list of at most 100 entries. Domains are trimmed, lowercased, IDNA encoded and deduplicated. An empty list leaves domains unrestricted. Invalid lists must be repaired even while SSO is disabled.
- A self-hosted instance with no stored sign-in method gets one on its next start, by the rules in [Sign-in methods](/operator/sign-in/#instances-that-already-exist). Until that write succeeds the instance behaves as an email instance.
- Missing registration URL and pending-registration lists mean empty lists. Invalid records are rejected rather than discarded. Timestamps require an explicit UTC marker or offset, and null is accepted only for nullable fields.
Explicit `false` values and supported null clears remain valid. Branding and integration strings retain their documented trimming and blank-value behaviour. Media size and lifetime adjustments also remain unchanged. These checks do not make a multi-section update atomic. Read back the result after a failed write before retrying.
## Limits
No environment variable changes an instance limit. Use the admin dashboard's Limit Config page or the [Admin API](/admin-api/). The thread and forum limits are fixed and have no limit key. [Thread lifecycle](/http-api/threads/#thread-lifecycle), [Thread members](/http-api/thread-members/) and [Forum channel fields](/http-api/forums/#forum-channel-fields) state them. Effective limits combine saved settings, deployment defaults and premium policy. Only the values you change are saved. Every other limit follows the deployment defaults and the premium model, so switching the model moves the untouched limits and keeps your edits. Reset to Defaults on a rule in the dashboard drops the edits saved for that rule. After a change is saved, every running `api` and `worker` process reloads the limits with no restart. Clients receive the [limit configuration object](/http-api/instance/#limit-configuration-object).
Request concurrency is separate from instance limits, and each process sets its own. All are optional.
#### `FLUXER_API_MAX_INFLIGHT_REQUESTS`
Default `512`. The API in-flight ceiling. Integer 1 to 100000, checked at startup.
#### `FLUXER_GATEWAY_HTTP_RPC_MAX_CONCURRENCY`
Default `512`. Gateway HTTP RPC concurrency.
#### `FLUXER_GATEWAY_NATS_RPC_MAX_HANDLERS`
Default `512`. Gateway NATS handler count.
#### `FLUXER_DISABLE_RATE_LIMITS`
Default `false`. Turns rate limits off. Read by the API and by the Gateway. The Gateway turns rate limits off only for the values `1`, `true` and `TRUE`. A test setting, which Compose does not forward.
#### `FLUXER_RELAX_REGISTRATION_RATE_LIMITS`
Default `false`. Loosens registration rate limits. A test setting, which Compose does not forward.
## Logging and build metadata
All are optional.
#### `LOG_LEVEL`
Defaults to `debug` in development, `info` otherwise. The Node log level. Read by `api` and `worker`.
#### `RUST_LOG`
Default `info`. The Rust log filter. Read by `media-proxy`, `app-proxy`, `admin`, `push`, and the internal services. An empty or invalid filter also gives `info`, and `off` silences them.
#### `FLUXER_GATEWAY_LOGGER_LEVEL`
Default `info`. The Gateway log level. An unrecognised level also gives `info`.
#### `LOGGER_LEVEL`
Falls back to `FLUXER_GATEWAY_LOGGER_LEVEL`. The Gateway log level at runtime. Overrides the prefixed name, and an unrecognised level falls back to it.
#### `BUILD_VERSION`
Default `dev`. The reported build. Baked into the images, so Compose does not set it. When `BUILD_VERSION` is unset or empty outside development, the process prints a warning.
#### `HOSTNAME`
Set by Docker. Node identity in metrics and logs.
#### `FLUXER_ENV`
Default `production` when `FLUXER_SELF_HOSTED` is `true`, otherwise `development`. The runtime mode. `development`, `production`, or `test`. Compose pins `production`, which gates Postgres validation and the admin cookie flags. On a self-hosted instance, `production` also enforces the [setup token](#setup-token).
#### `FLUXER_SELF_HOSTED`
Default `false`. The self-host switch. Compose sets `true`. It relaxes the production Postgres SSL requirement, seeds the limit tier, gates registration, donation and discovery controllers, keeps premium billing off until it is set up as [Payments](#payments) describes, turns blocklist feeds off, keeps an account closed for a broken rule for at least 14 days instead of 74 because there is no appeal route, and keeps hosted-only artwork from `fluxer.app` out of the app, webhook avatars and link embeds.
## Feature flags and development switches
All are optional.
#### `FLUXER_DISCOVERY_ENABLED`
Default `true`. The public guild [discovery](/http-api/discovery/) surface. With it off, discovery search, discovery join, and the guild discovery application routes return 400 `DISCOVERY_DISABLED`.
#### `FLUXER_DISCOVERY_MIN_MEMBER_COUNT`
Default `1`. Minimum members for discovery eligibility. Integer.
#### `FLUXER_DELETION_GRACE_PERIOD_HOURS`
Default `336`. How long a deleted account is recoverable. Forced to 0.01 hours when `FLUXER_TEST_MODE_ENABLED` is on.
#### `FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED`
Default `false`. Presigned attachment uploads. Only `api` reads it. Compose forwards it from `.env` to `api` with a default of `true`.
#### `FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED`
Default `true`. Presigned harvest downloads. Applies to the harvests bucket. A presigned link points at `FLUXER_S3_PUBLIC_ENDPOINT`, which in the bundled stack is the internal `http://seaweedfs:8333` that no browser reaches. Compose therefore forwards it from `.env` with a default of `false`. Turn it on once `FLUXER_S3_PUBLIC_ENDPOINT` names an address browsers can reach.
#### `FLUXER_API_STORAGE_CHANGE_FEED_ENABLED`
Default `false`. Whether `api` and `worker` publish a record of every object-store write and delete to a JetStream stream.
#### `FLUXER_API_STORAGE_CHANGE_FEED_STREAM`
Default `STORAGE_CHANGES`. The JetStream stream the feed publishes to. Letters, digits, underscores or hyphens, checked at startup only while the feed is on.
#### `FLUXER_API_STORAGE_CHANGE_FEED_SKIP_BUCKETS`
Defaults to the uploads bucket. Buckets the feed leaves out, comma separated. `none` leaves out no bucket.
#### `FLUXER_API_UNFURL_IGNORED_HOSTS`
Default empty. Hosts never unfurled. Comma separated, unvalidated.
#### `FLUXER_TEST_MODE_ENABLED`
Default `false`. Test mode. Collapses the deletion grace period. A test setting, which Compose does not forward.
#### `FLUXER_TEST_HARNESS_TOKEN`
No default. The test harness credential. A test setting, which Compose does not forward.
#### `FLUXER_VALIDATE_RESPONSES`
Defaults to on outside production. Response schema validation. Costs latency when forced on. A development setting, which Compose does not forward.
#### `FLUXER_KLIPY_API_KEY`
Default empty. The Klipy GIF provider key. GIF search reports itself unavailable while this and the admin dashboard key are both empty. Also read by `unfurl`, which accepts `KLIPY_API_KEY` as a fallback. Compose forwards this name from `.env`, and not the fallback.
#### `FLUXER_YOUTUBE_API_KEY`
Default empty. The YouTube Data API key. Also read by `unfurl`, which accepts `YOUTUBE_API_KEY` as a fallback. Compose forwards this name from `.env`, and not the fallback.
#### `FLUXER_CACHE_PURGE_ADAPTER`
Default `none`. How cached copies of deleted or replaced media are purged. `none`, `http` or `controller`. Anything else fails startup. With `none`, nothing is queued or sent.
#### `FLUXER_CACHE_PURGE_HTTP_ENDPOINT`
Default empty. Required when the adapter is `http`. Must be an absolute `http` or `https` URL without credentials, or startup fails. While purges are queued, the worker posts JSON `{"prefixes": [...]}` here every 10 seconds, each a media URL prefix without its scheme. Redirects are not followed. Any 2xx marks the batch done. Your endpoint then purges every cached URL under those prefixes from each proxy that caches media. A `400` or `422` moves the batch to the `cache_purge:rejected` key. Any other failure is retried on a later run.
#### `FLUXER_CACHE_PURGE_HTTP_TOKEN`
Default empty. The bearer token sent in the `Authorization` header of each purge request. Empty sends no header. When the adapter is `http`, a token with spaces, line breaks or non-ASCII characters fails startup.
#### `FLUXER_CACHE_PURGE_HTTP_TIMEOUT_MS`
Default `10000`. Purge request timeout. Accepts 1000 to 10000. The range is checked only when the adapter is `http`.
#### `FLUXER_CACHE_PURGE_CONTROLLER_ENDPOINT`
Default empty. Required when the adapter is `controller`. The purge URL of the Controller edge site that serves your media, in the form `https://<controller host>/v1/orgs/<org id>/sites/<site id>/purge`. Must be an absolute `https` URL without credentials, or startup fails. While purges are queued, the worker posts JSON `{"patterns": [...], "soft": false}` here every 10 seconds, at most 30 patterns per request, each a media URL prefix without its scheme followed by `*`. Any 2xx marks the patterns done. A `400` or `422` moves the batch to the `cache_purge:rejected` key. Any other failure retries the whole batch on a later run.
#### `FLUXER_CACHE_PURGE_CONTROLLER_TOKEN`
Default empty. Required when the adapter is `controller`. The Controller API key, sent as a bearer token. A token with spaces, line breaks or non-ASCII characters fails startup.
#### `FLUXER_CACHE_PURGE_CONTROLLER_TIMEOUT_MS`
Default `10000`. Timeout of each purge request. Accepts 1000 to 10000. The range is checked only when the adapter is `controller`.
#### `FLUXER_GEOIP_DB_PATH`
Default empty. The GeoIP database. A filesystem path, or an `s3://bucket/key` URL whose `download_path` query parameter is mandatory and must be absolute. Read by `api` and `worker`.
## Instance identity and branding
A product name or brand asset saved in the admin dashboard wins over its variable. All are optional.
A brand asset on another origin than the instance needs no CSP setting. `app-proxy` adds the origin of each of the five image URLs below to `img-src`, whether the URL is set here or in the dashboard. [Content Security Policy](#content-security-policy) has the timing.
`app-proxy` on a self-hosted instance also writes the product name, the icon, the favicon and the theme colour into the page it serves, so a browser shows them before the web app starts. The page title and the home screen name use the product name. The home screen icon uses the icon, or the favicon when no icon is set. The browser favicon and the `/favicon.ico` redirect use the favicon, or the icon when no favicon is set. The install manifest at `/manifest.json` takes the product name, the theme colour and the icon, and leaves out the Fluxer description. `/browserconfig.xml`, the Fluxer tile for legacy Edge, returns 404. `app-proxy` takes these values the same way as the image origins, from this environment at start and then from the dashboard.
#### `FLUXER_APP_PRODUCT_NAME`
Default `Fluxer`. The product name clients display and the API writes in its emails and report flows.
On a self-hosted instance, a name that is empty or `Fluxer` is replaced by the host name of the web app endpoint in everything the server writes: emails and their sender name, system messages and the name of the system user, report flows, enforcement notices, IP block errors, the name of the built-in admin application and donation checkout. Clients keep showing the saved name, except on the network blocked screen, which takes the name from the error. Set a name of your own to use it everywhere.
#### `FLUXER_APP_ICON_URL`
Default empty. The client icon.
#### `FLUXER_APP_SYMBOL_URL`
Default empty. The symbol mark.
#### `FLUXER_APP_LOGO_URL`
Default empty. The logo.
#### `FLUXER_APP_WORDMARK_URL`
Default empty. The wordmark.
#### `FLUXER_APP_FAVICON_URL`
Default empty. The favicon.
#### `FLUXER_APP_THEME_COLOR`
Default empty. The theme colour, as a hex value such as `#4641d9`. The web app uses it as its brand colour and as the browser theme colour. A value that is not `#rgb` or `#rrggbb` is ignored, and so is pure black.
#### `FLUXER_APP_STATUS_PAGE_URL`
Default empty. The public status page. The web app links to it when loading stalls, and polls its Instatus `summary.json` and `components.json` for incidents and maintenance. Empty removes the link and stops the polling. `app-proxy` does not add this origin to the CSP. A status page off `fluxerstatus.com` also needs it in `FLUXER_CSP_EXTRA_CONNECT_SRC`.
#### `FLUXER_APP_STATUS_PAGE_INCIDENT_HISTORY_URL`
Default empty. The incident history page of the status page.
#### `FLUXER_INSTANCE_SETUP_CONFIGURED`
Default `false`. Whether setup is marked complete. The stored `app_public_config` row wins whenever its `setup.configured` is a boolean. This variable supplies the default only while no such row exists, and only on a self-hosted instance. Off a self-hosted instance the state is always true whatever this says. Setting it to `true` on a new instance skips the setup wizard and opens registration, and nobody becomes admin by registering first. The admin account is then created by a [registration request](/http-api/authentication/#register-an-account) that includes `setup_token`.
<a id="setup-token"></a>
#### `FLUXER_SETUP_TOKEN`
Default empty. The setup token of a new self-hosted instance, 32 to 256 characters, and a value outside that range fails startup. Until the first admin account exists, only the registration that presents this token becomes the admin. While the instance is also unconfigured, every registration must present it. Unset, the API generates a token on the first start, stores only its hash and writes the token to its log once. A value set here always replaces the generated one. It is read by the `api` service only and is ignored once the first admin exists. Off a self-hosted instance it is ignored. The installer writes one into `.env`, and [Create the first admin](/operator/get-started/#first-run) has where the wizard asks for it.
#### `FLUXER_ACCOUNT_IDENTITY`
Default empty, which means `email` when the email switch is on and `username` otherwise. How members sign in on a new self-hosted instance, `username` or `email`. Case is ignored, and any other value fails startup. The API reads it once, on the first start of an instance with no stored sign-in method. Off a self-hosted instance the method is always `email`. [Choosing the method](/operator/sign-in/#choosing-the-method) and [Instances that already exist](/operator/sign-in/#instances-that-already-exist) have the rules.
#### `FLUXER_TAG_STYLE`
Default empty, which means `none`. How usernames are tagged on a new self-hosted email instance. `none` gives unique usernames with no tag, and `random` gives every account a random tag. A username instance always uses `none` and ignores this value. Case is ignored, and any other value fails startup. The API reads it once, together with `FLUXER_ACCOUNT_IDENTITY`. [Choosing the method](/operator/sign-in/#choosing-the-method) and [Instances that already exist](/operator/sign-in/#instances-that-already-exist) have the rules.
#### `FLUXER_AUTO_JOIN_INVITE_CODE`
Default empty. An invite every new account joins. Must be a live invite code.
#### `FLUXER_VISIONARIES_GUILD_ID`
Default empty. The guild that grants the visionary role. Snowflake.
#### `FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID`
Default empty. The role granted there. Snowflake.
Completing setup saves that state independently of the environment default. Later branding or legal updates preserve it. Setting `FLUXER_INSTANCE_SETUP_CONFIGURED` back to `false` does not reopen the wizard or restore the admin grant described in [Create the first admin](/operator/get-started/#first-run).
## API and worker settings
`FLUXER_API_WORKER_TASK` is required under `single_task`. The rest are optional.
#### `FLUXER_API_PORT`
Default `8080`. The API listen port. Compose sets `8080`, which the edge proxies to.
#### `FLUXER_API_HEADERS_TIMEOUT_MS`
Default `30000`. How long a client may take to send the request line and the headers. Integer 1000 to 3600000, checked at startup. It is clamped down to `FLUXER_API_REQUEST_TIMEOUT_MS`. Raising it alone does nothing.
#### `FLUXER_API_REQUEST_TIMEOUT_MS`
Default `120000`. How long a client may take over the whole request. Integer 1000 to 3600000, checked at startup. This is the one to raise for large uploads or high latency links.
#### `FLUXER_API_WORKER_MODE`
Default `all_lanes`. Which lanes the worker runs. `all_lanes`, `single_lane`, or `single_task`. Anything else fails startup. Compose sets `all_lanes`, because the stack runs one `worker`.
#### `FLUXER_API_WORKER_LANE`
No default. Which lane, under `single_lane`. `realtime`, `unfurl`, `lifecycle`, `batch`, or `crosspost`. Compose does not forward it. A deployment that splits the worker by lane needs a `crosspost` worker, or published announcements are never delivered.
#### `FLUXER_API_WORKER_TASK`
No default. Which task, under `single_task`. Must name a known task. Compose does not forward it.
#### `FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER`
No default. Whether the cron scheduler runs. Compose sets `true`. Without it no scheduled job runs anywhere.
#### `FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES`
No default. Per-lane concurrency. JSON object keyed by lane, `realtime`, `unfurl`, `lifecycle`, `batch`, or `crosspost`. Each value must be an integer of at least 1 or startup fails. Built-in concurrency is realtime 10, unfurl 20, lifecycle 8, batch 12, crosspost 8.
#### `FLUXER_NODE_EXTRA_CA_CERTS`
Default `/etc/ssl/certs/ca-certificates.crt`, the system bundle in the image. A PEM file of extra CA certificates that `api` and `worker` trust for every outbound TLS connection, such as SMTP, object storage, search and webhooks. Node trusts its own built-in roots plus this one file. A file you set replaces the system bundle, so append your CA to a copy of that bundle, mount it into both containers and point this at it. Compose passes it to `api` and `worker` as `NODE_EXTRA_CA_CERTS`, and keeps the image path when `.env` leaves it out. It reads the prefixed name so a `NODE_EXTRA_CA_CERTS` in the host shell does not reach the containers.
## Media Proxy settings
The Media Proxy takes uploads, transforms images, and serves media back. All are optional.
#### `FLUXER_MEDIA_PROXY_MODE`
Default `mp`. Which routes are served. `mp`, `static`, `upload`, or `relay`. `relay` serves the upload relay and the operator and internal endpoints, and returns 404 for every read. Anything else is a startup error. Also settable with `--mode`. Compose sets `upload`, which the upload relay needs.
#### `FLUXER_MEDIA_PROXY_HOST`
Default `0.0.0.0`. The bind address. Also settable with `--bind-host`. Compose does not set it, so the default applies.
#### `FLUXER_MEDIA_PROXY_PORT`
Default `8080`. The listen port. Also settable with `--port`. Compose sets `8080`, which the edge proxies to.
#### `FLUXER_MEDIA_PROXY_READ_ONLY`
Default `false`. Refuses writes. `--read-only` can force it on.
#### `FLUXER_MEDIA_PROXY_STORAGE_BACKEND`
Default `local`. Where objects live. `local` or `s3`. Compose sets `s3`, the bundled object store.
#### `FLUXER_MEDIA_PROXY_STORAGE_ROOT`
Default `./media_proxy_storage`. The local storage directory. Used only by the local backend, so Compose does not forward it.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES`
Default `524288000`. The upload size cap on both sides. Rust accepts 1 byte to 5 GiB and refuses to start when the value is above `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES`. The effective cap is the smaller of the token's value and this.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS`
Default `900`. Relay token lifetime. Read by the API alone.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_ENDPOINT`
Default `http://localhost:8088/media`. The relay URL handed to clients. Node side only. A trailing slash and a trailing `/v1/relay` are stripped. Compose forwards it from `.env`, and builds it from the public origin and `/media` when it is unset.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES`
Default empty. Countries that upload directly to S3. Node side only. Empty means every client uses the relay, which is what keeps the internal S3 address out of browsers.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_S3_TIMEOUT_MS`
Default `900000`. Relay upload timeout. Accepts 1000 to 3600000.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES`
Default `33554432`, 32 MiB. The largest declared upload the relay holds in memory so it can retry the S3 write. Accepts 0 to 268435456. A larger upload is streamed to S3 and gets no retry.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES`
Default `536870912`, 512 MiB. The memory all buffered relay uploads may hold at once. Accepts 0 to 8589934592. An upload that would pass it is streamed instead. The default equals the `512mb` that `FLUXER_MEDIA_PROXY_MEMORY_LIMIT` gives `media-proxy`. Lower this or raise that limit when many uploads arrive at once.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_DIR`
Defaults to the system temporary directory. Where relay bodies spool, and where `media-proxy` copies a stored audio or video object above 500 MiB when it has to read inside the file. That happens when the API asks for the media metadata or a frame of an upload or a stored object, and when an attachment read asks for a frame of a video. Must be writable.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_CHUNK_BYTES`
Default `1048576`. Spool chunk size. Accepts 64 KiB to 64 MiB.
#### `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES`
Default `8589934592`. Total spool ceiling, shared by relay bodies and the copies of stored audio and video above 500 MiB. A relay upload that does not fit beside the copies in progress fails. An upload larger than the ceiling is stored as a plain file without media metadata, and a stored video larger than the ceiling has no poster frame. Accepts 0 to 256 GiB, and must be at or above `FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES`, which puts the usable floor at 500 MiB by default.
Sending an audio or video attachment above 500 MiB holds up to three copies of it on local disk at once. `media-proxy` holds one in the spool directory, and `api` holds the download and the copy it rewrites without metadata in its temporary directory. Leave free space of three times the attachment size limit for each send you expect at the same moment.
`api` waits 30 seconds for the metadata of an upload up to 500 MiB. Above that it waits 30 seconds plus one second for each 16 MiB of the upload, about 94 seconds for 1 GiB and 542 seconds for 8 GiB. In that time `media-proxy` copies the upload out of object storage, reads it, and hashes it. A deployment that cannot sustain 16 MiB per second through those steps stores the upload as a plain file.
#### `FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS`
Defaults to available parallelism, clamped to 2 to 8. Concurrent image transforms. Accepts 1 to 128.
#### `FLUXER_MEDIA_PROXY_WORKER_QUEUE_CAPACITY`
Defaults to eight times the transform limit. Transform queue depth. Accepts 1 to 8192.
#### `FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_BYTES`
Default `268435456`. Transform cache size. Accepts 0 to 4 GiB.
#### `FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES`
Default `67108864`. Largest cacheable result. Accepts 0 to 512 MiB.
#### `FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_TTL_MS`
Default `120000`. Transform cache lifetime. Accepts 0 to 3600000.
#### `FLUXER_MEDIA_PROXY_SOCKET_IO_TIMEOUT_MS`
Default `30000`. Socket read and write timeout. Accepts 0 to 300000.
#### `FLUXER_MEDIA_PROXY_SHUTDOWN_GRACE_MS`
Default `30000`. Milliseconds allowed to finish requests and media transforms after SIGTERM or Ctrl-C. Accepts 0 to 300000. Exceeding the deadline exits the process with an error.
#### `FLUXER_MEDIA_PROXY_TRANSFORM_TIMEOUT_MS`
Default `15000`. Per-transform timeout. Accepts 1000 to 120000.
#### `FLUXER_MEDIA_PROXY_MAX_ENCODE_FRAMES`
Default `20000`. Animation frame ceiling. Accepts 1 to 100000.
#### `FLUXER_MEDIA_PROXY_MAX_ENCODE_DURATION_MS`
Default `30000`. Animation duration ceiling. Accepts 100 to 600000.
#### `FLUXER_NSFW_SERVICE_ENDPOINT`
Default empty. An external NSFW classifier. No such service ships with the stack.
#### `FLUXER_MEDIA_PROXY_NSFW_THRESHOLD`
Default `0.85`. The media-side NSFW cutoff. Accepts 0.0 to 1.0 and finite.
#### `FLUXER_MEDIA_PROXY_CORS_MODE`
Default `off`. The [cross-origin read policy](/media-proxy/overview/#cross-origin-reads). `off`, `report`, or `enforce`. Anything else is a startup error. `report` logs each read whose origin would be refused and changes no response. `enforce` refuses those reads with 403 and sets `Access-Control-Allow-Origin` to the request origin. When `FLUXER_MEDIA_PROXY_MODE` is `static` or `relay`, the Media Proxy ignores it. In those modes a bad value is no startup error.
#### `FLUXER_MEDIA_PROXY_CORS_ALLOWED_ORIGINS`
Default empty. Comma-separated origins that may read media through CORS, such as `https://chat.example.com`. Spaces around an entry and empty entries are ignored. Each entry is parsed as a URL and must be `http` or `https` with no credentials, query, fragment, or path beyond `/`. Its host must be an IPv4 address, a bracketed IPv6 address, or a domain name whose labels are ASCII letters, digits, and hyphens after parsing. So a wildcard such as `https://*.example.com`, a brace pattern, an underscore, a trailing dot on a domain name, or any non-ASCII character fails. Write an internationalised domain name in its punycode form. The one exception is the desktop app origin `fluxer-app://app`. It is accepted as an entry and is always allowed without one. Any failing entry makes the process exit. An entry is compared in the form a browser sends. `https://Chat.Example.Com:443/` matches `https://chat.example.com`. Required when `FLUXER_MEDIA_PROXY_CORS_MODE` is `report` or `enforce`. When the policy is `report` or `enforce`, the Media Proxy logs the parsed list once at startup. When `FLUXER_MEDIA_PROXY_MODE` is `static` or `relay`, the Media Proxy ignores it.
Compose forwards it from `.env`, and defaults it to the public origin, built from `FLUXER_PUBLIC_ORIGIN`, or from `FLUXER_PUBLIC_SCHEME`, `FLUXER_DOMAIN`, and `FLUXER_PUBLIC_PORT`. That origin is the only entry. The web app on the instance can then read its media. A value set in `.env` replaces the Compose value and must name the public origin.
#### `FLUXER_MEDIA_PROXY_ATTACHMENT_SIGNATURE_MODE`
Default `off`. The [signed attachment URL policy](/media-proxy/overview/#signed-attachment-urls). `off`, `report`, or `enforce`. An empty value means `off`, and anything else is a startup error. `report` logs each attachment read `enforce` would refuse and serves it. `enforce` refuses those reads with 404. Under both, a read whose signature is valid gets a `Cache-Control` lifetime that ends when the signature expires. `report` and `enforce` require `FLUXER_MEDIA_PROXY_ATTACHMENT_URL_SECRETS_BASE64`, and the Media Proxy logs the mode and the number of secrets once at startup. When `FLUXER_MEDIA_PROXY_MODE` is `static` or `relay`, the Media Proxy ignores it and the secrets. In those modes a bad value is no startup error. A self-hosted instance leaves it `off`, and signs nothing, until an operator turns it on.
Apply a change with `docker compose up -d media-proxy`. A cache or proxy in front of the instance has a switch of its own, and [Turning the signature policy on](/operator/reverse-proxy/#turning-the-signature-policy-on) gives the order to move the two in and what each pairing serves.
## Gateway settings
The Gateway is the WebSocket service clients hold open for live events. All are optional.
#### `FLUXER_GATEWAY_PORT`
Default `8771`. The listen port. Compose sets `8080`, which the edge proxies to.
#### `FLUXER_GATEWAY_ROLE`
Default `all`. Which subsystems this node runs. `websocket`, `sessions`, `presence`, `guilds`, `calls`, `push`, or `all`. An unrecognised value also becomes `all`. Compose does not forward it, because the stack runs one Gateway node.
#### `FLUXER_GATEWAY_MEDIA_PROXY_ENDPOINT`
Default `http://localhost:8088/media`. The public media URL used in payloads. Compose forwards it from `.env`, and uses `FLUXER_MEDIA_ENDPOINT` or the public media URL when it is unset.
#### `FLUXER_GATEWAY_STATIC_CDN_ENDPOINT`
Default `http://localhost:8088`. The public static origin used in payloads. Compose forwards it from `.env`, and uses `FLUXER_STATIC_CDN_ENDPOINT` or the public origin when it is unset.
#### `FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES`
Default `128`. Presence push buffer depth.
#### `FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES`
Default `1048576`. Presence push buffer size.
#### `FLUXER_GATEWAY_PUSH_ENROLLED_CLEAR_NOTIFICATIONS_ENABLED`
Default `true`. Whether the Gateway asks `push` to clear a channel's notifications once the channel is read.
#### `FLUXER_GATEWAY_PUSH_OUTBOX_REQUEST_TIMEOUT_MS`
Default `100000`. How long the Gateway waits on one push outbox request. Milliseconds.
#### `FLUXER_GATEWAY_HTTP_FAILURE_THRESHOLD`
Default `6`. Failures before the API circuit opens. Integer.
#### `FLUXER_GATEWAY_HTTP_RECOVERY_TIMEOUT_MS`
Default `15000`. How long the circuit stays open. Milliseconds.
#### `FLUXER_GATEWAY_CLUSTER_ENABLED`
Default `false`. BEAM clustering. Single-node by default. Compose forwards none of the clustering names, because the stack runs one Gateway node.
#### `FLUXER_GATEWAY_CLUSTER_DISCOVERY_DNS_NAME`
No default. The DNS name peers are discovered through. Needs clustering on.
#### `FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME`
No default. The node basename for discovery. Needs clustering on.
#### `FLUXER_GATEWAY_CLUSTER_DISCOVERY_POLL_INTERVAL_MS`
Default `5000`. Discovery poll interval. Milliseconds.
#### `FLUXER_GATEWAY_CLUSTER_STATIC_PEERS`
Default empty. A fixed peer list. Comma separated Erlang node names, capped at 256. An entry that is not a node name fails startup.
#### `FLUXER_ERLANG_NODE_NAME`
Default `[email protected]`. The BEAM node name. Must be resolvable by peers when clustering. Compose does not forward it.
#### `FLUXER_ERLANG_COOKIE`
No default. The BEAM distribution secret. The Gateway refuses to start without it, and Compose refuses to start the stack when `.env` lacks it. Anyone who reaches the distribution port with the value gets code execution, so keep the port unpublished.
#### `FLUXER_ERLANG_DIST_PORT`
Default `8081`. The BEAM distribution port. Not published or forwarded by Compose. Never expose it.
#### `FLUXER_ERLANG_SCHEDULERS`
Defaults to the container CPU count, clamped to 2 through 16. Normal scheduler count. Positive integer, passed to the BEAM as `+S N:N`. A value that is not a positive integer is ignored and the derivation runs instead.
#### `FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS`
Defaults to two thirds of the scheduler count, rounded up. Dirty CPU scheduler count. Positive integer, passed as `+SDcpu N:N`. Same fallback rule as `FLUXER_ERLANG_SCHEDULERS`.
#### `FLUXER_ERLANG_SCHEDULERS_MIN`
Default `2`. The floor of the scheduler clamp.
#### `FLUXER_ERLANG_SCHEDULERS_MAX`
Default `16`. The ceiling of the scheduler clamp. `.env.example` ships both names commented out.
The Gateway entrypoint derives the scheduler counts before the BEAM starts. It reads the container CPU quota, clamps the result between `FLUXER_ERLANG_SCHEDULERS_MIN` and `FLUXER_ERLANG_SCHEDULERS_MAX`, exports the answer as `FLUXER_ERLANG_SCHEDULERS`, and derives `FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS` from it. `vm.args.src` then substitutes both into `+S` and `+SDcpu`.
Setting `FLUXER_ERLANG_SCHEDULERS` pins the count and skips the clamp.
The Gateway protocol version is `1`. The Gateway answers any other value in `?v=` with 101, then a close frame reading `Invalid API version`. [Gateway overview](/gateway/overview/) has the close code.
## App proxy settings
`app-proxy` serves the web client. All are optional.
#### `FLUXER_APP_PROXY_HOST`
Default `0.0.0.0`. The bind address. The image sets the same value, and Compose does not set it.
#### `FLUXER_APP_PROXY_PORT`
Default `8080`. The listen port. Compose sets `8080`, which the edge proxies to.
#### `FLUXER_STATIC_DIR`
Default `./static`. Where the client's SPA bundle lives, unrelated to `static-proxy`. A path inside the image, so Compose does not forward it.
#### `FLUXER_APP_PROXY_INDEX_UPSTREAM_URL`
No default. An upstream to fetch `index.html` from. Leave unset for the shipped image.
#### `DISCOVERY_UPSTREAM_URL`
Default `http://localhost:8088/api/.well-known/fluxer`. Where the self-hosted image fetches the discovery document, which it reads only for the branding image URLs. Compose sets `http://edge:8088/.well-known/fluxer`. That internal listener supplies the client-IP header the API requires, so leave it pointed at the edge.
#### `DISCOVERY_REFRESH_INTERVAL_MS`
Default `60000`. How often discovery is refetched, in milliseconds, and so the longest a branding image saved in the admin dashboard waits for its origin to reach the CSP. A value under `1000` counts as `1000`. A failed fetch keeps the previous origins and is retried with a backoff of up to five minutes. Unprefixed name.
#### `FLUXER_APP_PROXY_SAME_ORIGIN_HOSTS`
Default empty. Hostnames, comma separated, without a scheme, port or path. When a request's `Host` is in the list, the page bootstrap points the web app and its API at that host, as `https://<host>` and `https://<host>/api`. Every other request gets the discovery values unchanged. Set it when the web app is served on more than one hostname and each hostname routes `/api` to the API. Invalid entries are logged and dropped.
#### `FLUXER_APP_PROXY_MANIFEST_SCOPE_EXTENSIONS`
Default empty. HTTPS origins, comma separated, without a path, query or credentials. `app-proxy` writes each one into the `scope_extensions` list of the web app manifest. An installed web app keeps those origins inside its window. Empty leaves the list empty. Each listed origin must serve `/.well-known/web-app-origin-association` naming this web app, or browsers ignore the entry. Invalid entries are logged and dropped.
## Admin settings
`admin` serves the dashboard at `FLUXER_ADMIN_BASE_PATH`, `/admin` by default. All are optional.
#### `FLUXER_ADMIN_HOST`
Default `0.0.0.0`. The bind address. Compose does not set it, so the default applies.
#### `FLUXER_ADMIN_PORT`
Default `3020`. The listen port. The image sets 8080 and Compose sets 8080, which the edge proxies to.
#### `FLUXER_ADMIN_BASE_PATH`
Default empty. The path the dashboard is mounted under. Write it with a leading slash and no trailing slash, such as `/panel`. The edge and the Compose-built `FLUXER_ADMIN_ENDPOINT` use the value as written, and only `admin` itself normalises it. The service serves at root and re-prefixes every emitted URL with this. Read by `admin` and the edge. Compose forwards it from `.env` with a default of `/admin`, hands the same value to the edge, which routes that path to `admin`, and builds the default `FLUXER_ADMIN_ENDPOINT` from it. A change moves the admin OAuth redirect with it, and takes `docker compose up -d`.
#### `FLUXER_ADMIN_OAUTH_REDIRECT_URI`
Defaults to the admin endpoint plus `/oauth2_callback`. The OAuth2 redirect. Must equal what the API derives from `FLUXER_ADMIN_ENDPOINT`, so Compose does not set it. The API does not read this name.
#### `FLUXER_ADMIN_OAUTH_CLIENT_ID`
Default `1234567890123456789`. The OAuth2 client id of `admin`. Must equal the admin application id built into the API, so Compose does not set it.
#### `FLUXER_BUILD_VERSION`
Defaults to the crate version. The reported build of `admin` and `app-proxy`. `BUILD_VERSION` is preferred over it. Compose does not set it.
## Content Security Policy
`app-proxy` builds the policy for the client HTML and the assets it serves. The client HTML policy allows each inline script by its SHA-256 hash. Each variable appends sources to one directive on top of the built-in ones. All are empty by default. Apply a change with `docker compose up -d app-proxy`.
`FLUXER_CSP_EXTRA_DEFAULT_SRC`, `FLUXER_CSP_EXTRA_CONNECT_SRC`, `FLUXER_CSP_EXTRA_IMG_SRC`, `FLUXER_CSP_EXTRA_MEDIA_SRC`, `FLUXER_CSP_EXTRA_FONT_SRC`, `FLUXER_CSP_EXTRA_SCRIPT_SRC`, `FLUXER_CSP_EXTRA_STYLE_SRC`, `FLUXER_CSP_EXTRA_FRAME_SRC`, `FLUXER_CSP_EXTRA_WORKER_SRC`, and `FLUXER_CSP_EXTRA_MANIFEST_SRC` take one or more sources separated by commas, spaces, tabs, or newlines. Blank entries and sources the directive already lists are dropped. `FLUXER_CSP_REPORT_URI` sets a single `report-uri` value.
`object-src`, `base-uri`, and `frame-ancestors` are fixed and have no override. `app-proxy` adds the static CDN endpoint and the media endpoint from its environment. The self-hosted image also adds the origin of each branding image to `img-src`. At start it takes them from `FLUXER_APP_ICON_URL`, `FLUXER_APP_SYMBOL_URL`, `FLUXER_APP_LOGO_URL`, `FLUXER_APP_WORDMARK_URL` and `FLUXER_APP_FAVICON_URL`. It then polls the discovery document every `DISCOVERY_REFRESH_INTERVAL_MS`, where a value saved in the admin dashboard wins. A dashboard change reaches the header within a minute by default with no restart. A stack on one hostname needs no extra sources. The usual reason to set one is a voice server on another hostname, which needs its WebSocket origin in `FLUXER_CSP_EXTRA_CONNECT_SRC`. [Voice media does not use the proxy](/operator/reverse-proxy/#voice-media-does-not-use-the-proxy) has that line in place.
A front proxy must not add a [Content-Security-Policy of its own](/operator/reverse-proxy/#send-no-content-security-policy-of-its-own).
## Keys in .env.example that no service reads
Docker Compose or the edge container consumes most of the names below, and each of those reaches a service under a different name. Setting one outside Compose, in Kubernetes or a systemd unit, does nothing. The [Kubernetes](/operator/kubernetes/) chart sets the names they stand for itself.
#### `FLUXER_DOMAIN`
Interpolated into `FLUXER_BASE_DOMAIN` and into the derived URL strings.
#### `COMPOSE_FILE`
Read by Docker Compose to select the proxy overlay.
#### `FLUXER_EDGE_SITE_ADDRESS`
Read by the edge container only, and only in the bundled layout. Both overlays overwrite it, `docker-compose.proxy.yml` with `:8080` and `tunnel.compose.yml` with `:80`.
#### `FLUXER_CADDY_SITE_ADDRESS`
Read by Docker Compose as the default for `FLUXER_EDGE_SITE_ADDRESS` when that name is unset.
#### `FLUXER_EDGE_TRUSTED_PROXIES`, `FLUXER_EDGE_ENCODE` and `FLUXER_EDGE_GLOBAL_OPTIONS`
Read by the edge container only. They take effect on `docker compose up -d edge`.
#### `FLUXER_EDGE_BIND`
Used as the host side of the overlay's port mapping.
#### `FLUXER_HTTP_PORT` and `FLUXER_HTTPS_PORT`
Used as the host side of the edge's published ports.
#### `FLUXER_REGISTRY_OWNER`, `FLUXER_REGISTRY` and `FLUXER_IMAGE_TAG`
Image name selection.
#### The third-party image names
Image name selection for the bundled services, listed under [Images](#images).
#### `POSTGRES_PASSWORD`
Becomes `FLUXER_POSTGRES_PASSWORD` and the Postgres image's own password.
#### `MEILI_MASTER_KEY`
Becomes `FLUXER_SEARCH_API_KEY` and the Meilisearch image's own key.
#### `FLUXER_S3_ACCESS_KEY` and `FLUXER_S3_SECRET_KEY`
Become `FLUXER_S3_ACCESS_KEY_ID` and `FLUXER_S3_SECRET_ACCESS_KEY`, and the identity `seaweedfs-init` installs.
#### `LIVEKIT_API_KEY` and `LIVEKIT_API_SECRET`
Become `FLUXER_LIVEKIT_API_KEY` and `FLUXER_LIVEKIT_API_SECRET`, and LiveKit's own `LIVEKIT_KEYS`.
#### The extra CA bundle
`FLUXER_NODE_EXTRA_CA_CERTS` becomes `NODE_EXTRA_CA_CERTS` in `api` and `worker`.
#### The LiveKit configuration names
`FLUXER_LIVEKIT_LOG_LEVEL`, `FLUXER_LIVEKIT_TCP_PORT`, `FLUXER_LIVEKIT_UDP_PORT`, `FLUXER_LIVEKIT_USE_EXTERNAL_IP`, `FLUXER_LIVEKIT_NODE_IP`, `FLUXER_LIVEKIT_STUN_PRIMARY` and `FLUXER_LIVEKIT_STUN_SECONDARY` are written into `LIVEKIT_CONFIG`, and the two ports also into the published port mappings.
#### The per-service pool sizes
`FLUXER_API_POSTGRES_MAX_CONNECTIONS`, `FLUXER_WORKER_POSTGRES_MAX_CONNECTIONS`, `FLUXER_USERS_SHARD_POSTGRES_MAX_CONNECTIONS` and `FLUXER_MESSAGES_SHARD_POSTGRES_MAX_CONNECTIONS` each become `FLUXER_POSTGRES_MAX_CONNECTIONS` in their own service.
#### Memory, tuning, health checks and restarts
The names under [Resources](#resources) and [Health checks and restarts](#health-checks-and-restarts) are read by Docker Compose, which hands them to the engine or to the bundled services under their own names.
## Keys Compose does not forward
Setting a name below in `.env` has no effect, and each group gives the reason. For a name Compose fills from another name, set that other name. Add any other through a Compose override file, as [Keep a local compose change](/operator/upgrading/#keep-a-local-compose-change) describes.
#### Names Compose fills from another name
Compose writes each of these from the `.env` name given, so setting the name itself in `.env` has no effect. `FLUXER_POSTGRES_PASSWORD` comes from `POSTGRES_PASSWORD`. `FLUXER_POSTGRES_MAX_CONNECTIONS` comes from `FLUXER_API_POSTGRES_MAX_CONNECTIONS`, `FLUXER_WORKER_POSTGRES_MAX_CONNECTIONS`, `FLUXER_USERS_SHARD_POSTGRES_MAX_CONNECTIONS` or `FLUXER_MESSAGES_SHARD_POSTGRES_MAX_CONNECTIONS`, one per service. `FLUXER_S3_ACCESS_KEY_ID` comes from `FLUXER_S3_ACCESS_KEY`, and `FLUXER_S3_SECRET_ACCESS_KEY` from `FLUXER_S3_SECRET_KEY`. `FLUXER_SEARCH_API_KEY` comes from `MEILI_MASTER_KEY`. `FLUXER_LIVEKIT_API_KEY` comes from `LIVEKIT_API_KEY`, and `FLUXER_LIVEKIT_API_SECRET` from `LIVEKIT_API_SECRET`. `FLUXER_BASE_DOMAIN` comes from `FLUXER_DOMAIN`.
#### Fixed wiring
`FLUXER_ENV`, `FLUXER_SELF_HOSTED`, `FLUXER_DATABASE_BACKEND`, the listen ports `FLUXER_API_PORT`, `FLUXER_GATEWAY_PORT`, `FLUXER_MEDIA_PROXY_PORT`, `FLUXER_APP_PROXY_PORT` and `FLUXER_ADMIN_PORT`, the internal endpoints `FLUXER_INTERNAL_API_ENDPOINT`, `FLUXER_INTERNAL_MEDIA_PROXY_ENDPOINT` and `FLUXER_MEDIA_PROXY_ENDPOINT`, the `FLUXER_API_ENDPOINT` of `admin`, `FLUXER_SVC_NAME`, `FLUXER_SVC_MODE`, `FLUXER_SVC_SHARD_ID`, `FLUXER_SVC_SHARD_COUNT`, `FLUXER_API_WORKER_MODE`, `FLUXER_API_WORKER_ENABLE_CRON_SCHEDULER`, `FLUXER_MEDIA_PROXY_MODE` and `FLUXER_MEDIA_PROXY_STORAGE_BACKEND`. Compose sets each to a fixed value that the edge, the health checks or another service depend on. A different value gives no working stack.
#### Names the stack has no use for
`FLUXER_MEDIA_PROXY_HOST`, `FLUXER_APP_PROXY_HOST`, `FLUXER_ADMIN_HOST` and `FLUXER_PUSH_SERVICE_HOST`, whose default already listens on every interface. `FLUXER_PUSH_SERVICE_PORT`, whose default the health check of `push` reads. `FLUXER_SVC_LISTEN_HOST` and `FLUXER_SVC_PORT`, which the health checks address. `FLUXER_API_WORKER_LANE` and `FLUXER_API_WORKER_TASK`, which only the single-lane worker modes read. `FLUXER_MEDIA_PROXY_STORAGE_ROOT`, which only the local backend reads. `FLUXER_S3_BUCKET_STATIC`, which only the `static` mode of `media-proxy` reads. `FLUXER_GATEWAY_ROLE`, the `FLUXER_GATEWAY_CLUSTER_` names, `FLUXER_ERLANG_NODE_NAME` and `FLUXER_ERLANG_DIST_PORT`, since the stack runs one Gateway node. `FLUXER_GATEWAY_API_RPC_ENDPOINT`, which the Gateway derives from the internal API address. `FLUXER_SNOWFLAKE_SERVICE_SUBJECT`, `FLUXER_USERS_SERVICE_SUBJECT` and `FLUXER_GIF_SERVICE_SUBJECT`, which have to match the subjects the internal services listen on. `FLUXER_ADMIN_OAUTH_REDIRECT_URI`, which has to match what the API derives. `FLUXER_ADMIN_OAUTH_CLIENT_ID`, which has to match the admin application id built into the API. The relay-mode names of `push`, `FLUXER_PUSH_RELAY_MAX_CONCURRENT`, `FLUXER_PUSH_RELAY_MAX_BODY_BYTES`, `FLUXER_PUSH_RELAY_TRUSTED_PROXY_HOPS`, `FLUXER_PUSH_RELAY_SOURCE_BUCKET_ENABLED` and the `_ENTRIES`, `_PER_MINUTE` and `_BURST` names under `FLUXER_PUSH_RELAY_TOKEN_BUCKET` and `FLUXER_PUSH_RELAY_SOURCE_BUCKET`, since the stack runs `push` in delivery mode. `FLUXER_STATIC_DIR`, a path inside the image. `FLUXER_DOCS_ENDPOINT`, which only the docs server reads.
#### Allocator and image library tuning
`LD_PRELOAD` and `MALLOC_CONF` in `media-proxy` and `app-proxy`, and `VIPS_CONCURRENCY`, `VIPS_DISC_THRESHOLD` and `G_MESSAGES_DEBUG` in `media-proxy`. Each image sets them. `LD_PRELOAD` loads jemalloc and `MALLOC_CONF` tunes it. `VIPS_CONCURRENCY` stays at `1` on purpose, and `FLUXER_MEDIA_PROXY_MAX_NATIVE_TRANSFORMS` is the setting for image work in parallel. `VIPS_DISC_THRESHOLD`, `1g` in the image, is the image size above which libvips decodes to disk instead of memory. Lower it when a small `FLUXER_MEDIA_PROXY_MEMORY_LIMIT` runs out of memory.
#### Build and platform metadata
`BUILD_VERSION` and `FLUXER_BUILD_VERSION` describe the shipped build. `POD_NAME` and `HOSTNAME` come from the platform.
#### Test and development switches
`FLUXER_TEST_MODE_ENABLED`, `FLUXER_TEST_HARNESS_TOKEN`, `FLUXER_DISABLE_RATE_LIMITS`, `FLUXER_VALIDATE_RESPONSES` and `FLUXER_RELAX_REGISTRATION_RATE_LIMITS`. None of them belongs on a production instance.
#### The Cassandra names
Every `FLUXER_CASSANDRA_` name. The stack runs Postgres.
#### Older names of a forwarded setting
`FLUXER_NATS_CORE_URL`, `KLIPY_API_KEY` and `YOUTUBE_API_KEY`. A service reads each only when the name it replaces is unset or empty, and Compose forwards that name instead.
#### Individual Stripe price names
The `FLUXER_STRIPE_PRICE_MONTHLY_`, `FLUXER_STRIPE_PRICE_YEARLY_`, `FLUXER_STRIPE_PRICE_GIFT_1_MONTH_` and `FLUXER_STRIPE_PRICE_GIFT_1_YEAR_` names, one per currency. `FLUXER_STRIPE_PRICES` holds the same prices in one JSON object, and the admin dashboard can replace them.
## Runtime settings in the admin dashboard
The admin dashboard at `FLUXER_ADMIN_BASE_PATH` stores these in the database. They apply without recreating containers, and a value set here wins over the matching environment variable.
#### Instance Config, Public App Identity
Product name, client-visible brand assets, the terms, privacy, and community guidelines links, and the setup state in the instance discovery document. No environment variable sets a legal link. On a self-hosted instance, enforcement notices link a rule to the terms or guidelines page set here, without an anchor, and show the rule title alone when no page is set. The clients and report flows show no guidelines link until one is set. Self-hosted emails, enforcement notices and IP block errors name no contact address and no appeal route, and no setting adds one. The emails, the report flows, the IP block errors, the title of an APNs alert that has none, and the built-in admin application use the product name set here, with the [self-hosted host name rule](#instance-identity-and-branding), and a rename applies without a restart.
#### Instance Config, Registration Controls
Registration mode of `open`, `approval`, or `closed`, admin-issued registration URLs, and pending approval requests.
#### Instance Config, Bot protection
The [ALTCHA](/topics/captcha/) proof-of-work check on sign-up, login, password recovery, and a few other gated operations. It is on by default. Turn it off here, or change its cost and maximum counter.
#### Instance Config, Community & Policy
Single-community mode, direct messages and friends, the premium model, and optional embed services.
#### Instance Config, Runtime Integrations
The Klipy GIF key, the YouTube Data API key, email delivery with an SMTP connection test, and Bluesky OAuth. Email delivery and its test show only on an email instance.
#### Instance Config, Premium & billing
The premium tier name and information link, the billing switch, the Stripe secret key and webhook secret, the webhook URL to register in Stripe, and the price catalogue with its default currency, country currencies and legacy prices. See [Payments](#payments).
#### Instance Config, Media Expiry
Size-based attachment lifetimes. The built-in default is on.
Turning expiry off hides every expiry date at once and stops deletion. The stored dates are kept.
On a self-hosted instance, changes also apply to attachments that were uploaded earlier. Turning expiry on, or changing a size or lifetime rule, starts a background job that works through existing messages in chunks and recalculates the expiry of their attachments. No attachment is deleted while that job runs. An attachment whose date passed while expiry was off, or that the new settings would expire sooner, is kept for at least the renew window first. Until the job reaches a message, its attachments show the state they had before, so one whose date passed while expiry was off shows as expired until then.
The official hosted instance does not run that job. There, a change applies to attachments uploaded from then on, and deletion of expired attachments is never paused.
#### Instance Config, Gateway Rollout Configuration
Session and guild rollout percentages, RPC timeouts, and Gateway concurrency.
#### Instance Config, Single Sign-On (SSO)
OIDC-style SSO for the client and admin apps, and whether SSO is enforced.
#### Limit Config
The [instance limits](/http-api/instance/#limit-configuration-object) published to clients.
#### Voice Regions and Voice Servers
The voice regions offered and the servers behind them.
#### Admin API Keys
Credentials for the [Admin API](/admin-api/).
These settings exist only in the dashboard: attachment decay, which defaults to on, and the inactivity deletion threshold, which defaults to 365 days.
## Services
The stack runs its containers on one Docker bridge network, which is private to the stack.
| Service | Image | What it does |
| --- | --- | --- |
| edge | caddy:2.11-alpine | TLS and path routing, the only HTTP entry point |
| app-proxy | fluxer-app-proxy-self-hosted | Serves the web client and builds its CSP header |
| static-proxy | fluxer-static | Serves the static asset bundle |
| api | fluxer-api | The HTTP API |
| worker | fluxer-api | Background lanes and the cron scheduler |
| gateway | fluxer-gateway | The Gateway WebSocket |
| media-proxy | fluxer-media-proxy | Uploads, transforms, and media delivery |
| push | fluxer-push | Push notification delivery |
| admin | fluxer-admin | The admin dashboard |
| snowflakes, snowflakes-shard | fluxer-snowflakes | Identifier allocation |
| users, users-shard | fluxer-users | User reads and writes |
| messages, messages-shard | fluxer-messages | Message reads and writes |
| gifs, gifs-shard | fluxer-gifs | GIF provider access |
| unfurl, unfurl-shard | fluxer-unfurl | Link unfurling |
| postgres | postgres:16-alpine | The database |
| valkey | valkey/valkey:9.1-alpine | The key-value store and pub/sub bus, and the deletion queues |
| nats | nats:2.14-alpine | Core messaging, and the JetStream streams holding queued background jobs |
| meilisearch | getmeili/meilisearch:v1.53 | The search index |
| seaweedfs | chrislusf/seaweedfs:4.47 | S3-compatible object storage |
| seaweedfs-init | chrislusf/seaweedfs:4.47 | Creates the buckets and the S3 identity, then exits |
| livekit | livekit/livekit-server:v1.12.0 | Voice and video |
The third-party images above are the defaults of the names under [Images](#images).
The edge and LiveKit are the only services that publish ports. The edge publishes 80/tcp, 443/tcp, 443/udp, or one plain-HTTP port under the overlay. LiveKit publishes 7881/tcp and 7882/udp. Everything else is reachable only over the bridge network.
Most settings reach `api`, `worker`, `gateway`, `media-proxy`, `push`, `admin` and the internal services through one shared environment block, so one line in `.env` reaches every one of them that reads it. `app-proxy` takes its own list, since it reads the static, media and S3 upload origins, the branding image URLs and its own names. The edge takes the `FLUXER_EDGE_` variables and `FLUXER_ADMIN_BASE_PATH`. The third-party services take their image name, their memory limit, and their own tuning: `postgres` the password, database, role and `FLUXER_POSTGRES_` server settings, `meilisearch` the master key and `FLUXER_MEILISEARCH_` values, `valkey` the `FLUXER_VALKEY_` values, `seaweedfs` its heap ceiling and telemetry switch, and `livekit` the key pair, the port, STUN and log level variables. `static-proxy` reads no environment variables.
The internal services each run a router, which takes requests and holds no state, and one shard, which holds the caches and the database connections. The shipped stack fixes `FLUXER_SVC_SHARD_COUNT` at `1`.
## Resources
Every service has a memory limit and four also have a memory reservation, all under `deploy.resources`. Compose reads `gb` as 1024 MiB and `mb` as 1 MiB, so `5gb` is 5368709120 bytes. Plain `docker compose up` applies both keys on a single host, with no Swarm and no `--compatibility` flag. The engine rejects any limit below `6mb`, and rejects a limit lower than the same service's reservation with `Minimum memory limit can not be less than memory reservation limit`.
A limit is a ceiling. The limits below sum to 18.5 GiB and the stack does not need a host that large, because each container uses only the memory it allocates, up to its limit.
`deploy.resources.reservations.memory` becomes the container's cgroup v2 `memory.low`, which biases kernel reclaim towards other containers under host pressure. It reserves nothing on its own.
All are optional.
#### `FLUXER_CADDY_MEMORY_LIMIT`
Default `256mb`. The ceiling for `edge`.
#### `FLUXER_POSTGRES_MEMORY_LIMIT`
Default `5gb`. The ceiling for `postgres`. Must be at or above `FLUXER_POSTGRES_MEMORY_RESERVATION` or the container fails to create. Shared memory in use under `FLUXER_POSTGRES_SHM_SIZE` is charged against it.
#### `FLUXER_VALKEY_MEMORY_LIMIT`
Default `256mb`. The ceiling for `valkey`. Must stay above `FLUXER_VALKEY_MAXMEMORY`, which bounds the stored dataset alone.
#### `FLUXER_NATS_MEMORY_LIMIT`
Default `256mb`. The ceiling for `nats`. Covers JetStream file store metadata as well as the core server.
#### `FLUXER_MEILISEARCH_MEMORY_LIMIT`
Default `1536mb`. The ceiling for `meilisearch`. Indexing a backlog of a few hundred thousand messages with two indexing threads peaks near 1 GB, so keep this above 1 GB.
#### `FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY`
Default `256mb`. Becomes `MEILI_MAX_INDEXING_MEMORY`. The memory the indexer may use for one batch. Meilisearch uses more than this while indexing, because every indexing thread also has buffers of its own. Lower it whenever you lower `FLUXER_MEILISEARCH_MEMORY_LIMIT`.
#### `FLUXER_MEILISEARCH_MAX_INDEXING_THREADS`
Default `2`. Becomes `MEILI_MAX_INDEXING_THREADS`. Without it Meilisearch indexes on half the host's CPU threads, and each thread can use up to 150 MB of buffers, so a host with many cores runs out of memory under a container limit that suits a small one. Raise it only together with `FLUXER_MEILISEARCH_MEMORY_LIMIT`.
#### `FLUXER_MEILISEARCH_ENV`
Default `production`. Becomes `MEILI_ENV`. `production` requires the master key on every request.
#### `FLUXER_MEILISEARCH_NO_ANALYTICS`
Default `true`. Becomes `MEILI_NO_ANALYTICS`, which keeps Meilisearch from sending usage data.
#### `FLUXER_SEAWEEDFS_MEMORY_LIMIT`
Default `2gb`. The ceiling for `seaweedfs`. One process runs the master, the volume server and the S3 gateway. The peak is the parts of one upload in flight at once, which the client uploads in parallel: a 500 MB attachment is 20 parts of 25 MB.
#### `FLUXER_SEAWEEDFS_GOMEMLIMIT`
Default `1536MiB`. The heap ceiling the Go runtime collects against. Go cannot see the container limit, so without this value an upload burst grows the heap past `FLUXER_SEAWEEDFS_MEMORY_LIMIT` and the kernel OOM-kills the container mid-upload with exit 137. Raising `FLUXER_SEAWEEDFS_MEMORY_LIMIT` on its own does not fix that, because the runtime grows to fill whatever it is given. Keep this near three quarters of the container limit and move the two together.
#### `FLUXER_SEAWEEDFS_TELEMETRY`
Default `false`. Becomes the `-master.telemetry` flag of `seaweedfs`, which reports usage to the SeaweedFS project when on.
#### `FLUXER_SEAWEEDFS_VOLUME_GROWTH`
Default `1`. Becomes `WEED_MASTER_VOLUME_GROWTH_COPY_1`, the number of volumes SeaweedFS creates at once when a bucket runs out of room. Each volume reserves a 1 GB slot of free disk, and the stack writes to four buckets. SeaweedFS's own default of 7 needs about 28 GB free before every bucket has a volume, so on a smaller disk the first bucket takes every slot and uploads fail with `No writable volumes and no free volumes left`.
#### `FLUXER_SEAWEEDFS_INIT_MEMORY_LIMIT`
Default `128mb`. The ceiling for `seaweedfs-init`. A one-shot container that exits, so it never overlaps steady state.
#### `FLUXER_LIVEKIT_MEMORY_LIMIT`
Default `512mb`. The ceiling for `livekit`. Grows with the number of concurrent voice publishers.
#### `FLUXER_API_MEMORY_LIMIT`
Default `2560mb`. The ceiling for `api`. Node derives its heap ceiling from this at about half, and the derivation stops falling at or below `512mb`.
#### `FLUXER_WORKER_MEMORY_LIMIT`
Default `2560mb`. The ceiling for `worker`. Same Node derivation as `api`, applied to every background lane at once.
#### `FLUXER_GATEWAY_MEMORY_LIMIT`
Default `1gb`. The ceiling for `gateway`. The BEAM has no heap ceiling of its own, so this limit is the only bound on the Gateway.
#### `FLUXER_MEDIA_PROXY_MEMORY_LIMIT`
Default `512mb`. The ceiling for `media-proxy`. Image and video transforms decode into this ceiling, so a large upload is what reaches this limit.
#### `FLUXER_PUSH_MEMORY_LIMIT`
Default `256mb`. The ceiling for `push`. Raise it alongside `FLUXER_PUSH_SERVICE_QUEUE_CAPACITY` or `FLUXER_PUSH_SERVICE_SEND_CONCURRENCY`.
#### `FLUXER_STATIC_PROXY_MEMORY_LIMIT`
Default `256mb`. The ceiling for `static-proxy`. The service reads no environment variables and serves files only.
#### `FLUXER_APP_PROXY_MEMORY_LIMIT`
Default `256mb`. The ceiling for `app-proxy`. It keeps the branding image origins from discovery in memory and reads no database.
#### `FLUXER_SNOWFLAKES_MEMORY_LIMIT`
Default `128mb`. The ceiling for `snowflakes`. A router holds no shard state.
#### `FLUXER_SNOWFLAKES_SHARD_MEMORY_LIMIT`
Default `256mb`. The ceiling for `snowflakes-shard`. Holds the identifier buffer sized by `FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE`.
#### `FLUXER_USERS_MEMORY_LIMIT`
Default `128mb`. The ceiling for `users`. A router holds no shard state.
#### `FLUXER_USERS_SHARD_MEMORY_LIMIT`
Default `256mb`. The ceiling for `users-shard`. Holds the read cache bounded by `FLUXER_SVC_CACHE_MAX_ENTRIES`, which defaults to 100000 entries.
#### `FLUXER_GIFS_MEMORY_LIMIT`
Default `128mb`. The ceiling for `gifs`. A router holds no shard state.
#### `FLUXER_GIFS_SHARD_MEMORY_LIMIT`
Default `256mb`. The ceiling for `gifs-shard`. Lower than the 536870912 byte default of `FLUXER_GIFS_SHARD_CACHE_MAX_BYTES`.
#### `FLUXER_MESSAGES_MEMORY_LIMIT`
Default `128mb`. The ceiling for `messages`. A router holds no shard state.
#### `FLUXER_MESSAGES_SHARD_MEMORY_LIMIT`
Default `256mb`. The ceiling for `messages-shard`. The shard has a Postgres pool of 20 connections alongside the read cache.
#### `FLUXER_UNFURL_MEMORY_LIMIT`
Default `128mb`. The ceiling for `unfurl`. A router holds no shard state.
#### `FLUXER_UNFURL_SHARD_MEMORY_LIMIT`
Default `256mb`. The ceiling for `unfurl-shard`. Fetches remote pages, so a slow upstream holds bytes for the length of the fetch.
#### `FLUXER_ADMIN_MEMORY_LIMIT`
Default `256mb`. The ceiling for `admin`. Serves the dashboard and proxies no media.
A reservation goes to the services whose loss takes the instance down, so the kernel reclaims from everything else first. All are optional.
#### `FLUXER_POSTGRES_MEMORY_RESERVATION`
Default `3gb`. The reclaim floor for `postgres`. Must be at or below `FLUXER_POSTGRES_MEMORY_LIMIT`. Lowering the limit alone fails container creation.
#### `FLUXER_API_MEMORY_RESERVATION`
Default `1gb`. The reclaim floor for `api`. Must be at or below `FLUXER_API_MEMORY_LIMIT`.
#### `FLUXER_WORKER_MEMORY_RESERVATION`
Default `1gb`. The reclaim floor for `worker`. Must be at or below `FLUXER_WORKER_MEMORY_LIMIT`.
#### `FLUXER_GATEWAY_MEMORY_RESERVATION`
Default `384mb`. The reclaim floor for `gateway`. Must be at or below `FLUXER_GATEWAY_MEMORY_LIMIT`.
Node sizes its own old-space heap at roughly half the container limit, with a floor near 259 MB. A container limit of `2560mb` gives a 1328 MB heap ceiling, `1280mb` gives 664 MB, `768mb` gives 396 MB, and every limit at or under `512mb` gives the same 259 MB. Pin the value only to move it away from that derivation. All are optional.
#### `FLUXER_API_NODE_HEAP_MB`
No default. The V8 old-space ceiling for `api`, in MB. Appended to `NODE_OPTIONS` as `--max-old-space-size` only when non-empty. Keep it below `FLUXER_API_MEMORY_LIMIT`.
#### `FLUXER_WORKER_NODE_HEAP_MB`
No default. The V8 old-space ceiling for `worker`, in MB. Same rule against `FLUXER_WORKER_MEMORY_LIMIT`.
#### `FLUXER_API_NODE_OPTIONS`
No default. Extra Node flags for `api`, such as `--dns-result-order=ipv4first`. Compose reads it from `.env` and appends it to `NODE_OPTIONS` after `--enable-source-maps` and the heap flag, only when non-empty.
#### `FLUXER_WORKER_NODE_OPTIONS`
No default. Extra Node flags for `worker`. Same rule as `FLUXER_API_NODE_OPTIONS`.
The names on the Postgres command line tune the bundled server. Keep them consistent with `FLUXER_POSTGRES_MEMORY_LIMIT`. All are optional.
#### `FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS`
Default `150`. The server-wide connection ceiling. The shipped pools total 90, from 25 each for `api` and `worker` and 20 each for `messages-shard` and `users-shard`. A value under about 95 exhausts the server before the pools fill, and the connections it turns away are refused with `too many clients already`.
#### `FLUXER_POSTGRES_SHARED_BUFFERS`
Default `512MB`. The shared buffer pool. Allocated at server start, so it is charged to the container whether or not it is used.
#### `FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE`
Default `2GB`. The planner's assumption about disk cache. Allocates nothing. Lowering it makes the planner prefer sequential scans and frees no memory.
#### `FLUXER_POSTGRES_WORK_MEM`
Default `8MB`. The per-operation sort and hash budget. Charged per sort or hash node, so one complex query can spend several multiples of it.
#### `FLUXER_POSTGRES_MAINTENANCE_WORK_MEM`
Default `256MB`. The budget for one VACUUM, CREATE INDEX, or ALTER TABLE. One such operation at a time normally holds it.
#### `FLUXER_POSTGRES_AUTOVACUUM_WORK_MEM`
Default `128MB`. The budget for each autovacuum worker. Postgres runs three workers by default, so budget three times this value.
#### `FLUXER_POSTGRES_SHM_SIZE`
Default `1gb`. The shared memory of the `postgres` container, used by parallel queries and parallel maintenance. Keep it well above `FLUXER_POSTGRES_MAINTENANCE_WORK_MEM`. A manual VACUUM sizes its shared memory from that budget, and fails with `could not resize shared memory segment` when it does not fit.
The rest of the Postgres command line has one variable per setting.
- `FLUXER_POSTGRES_RANDOM_PAGE_COST`: default `1.1`, sets `random_page_cost`.
- `FLUXER_POSTGRES_EFFECTIVE_IO_CONCURRENCY`: default `200`, sets `effective_io_concurrency`.
- `FLUXER_POSTGRES_DEFAULT_STATISTICS_TARGET`: default `200`, sets `default_statistics_target`.
- `FLUXER_POSTGRES_JIT`: default `off`, sets `jit`.
- `FLUXER_POSTGRES_MIN_WAL_SIZE`: default `512MB`, sets `min_wal_size`.
- `FLUXER_POSTGRES_MAX_WAL_SIZE`: default `2GB`, sets `max_wal_size`.
- `FLUXER_POSTGRES_CHECKPOINT_COMPLETION_TARGET`: default `0.9`, sets `checkpoint_completion_target`.
- `FLUXER_POSTGRES_WAL_BUFFERS`: default `16MB`, sets `wal_buffers`.
- `FLUXER_POSTGRES_WAL_COMPRESSION`: default `zstd`, sets `wal_compression`.
- `FLUXER_POSTGRES_BGWRITER_DELAY`: default `50ms`, sets `bgwriter_delay`.
- `FLUXER_POSTGRES_BGWRITER_LRU_MAXPAGES`: default `1000`, sets `bgwriter_lru_maxpages`.
- `FLUXER_POSTGRES_AUTOVACUUM_VACUUM_SCALE_FACTOR`: default `0.05`, sets `autovacuum_vacuum_scale_factor`.
- `FLUXER_POSTGRES_AUTOVACUUM_ANALYZE_SCALE_FACTOR`: default `0.02`, sets `autovacuum_analyze_scale_factor`.
- `FLUXER_POSTGRES_AUTOVACUUM_VACUUM_COST_LIMIT`: default `2000`, sets `autovacuum_vacuum_cost_limit`.
- `FLUXER_POSTGRES_TRACK_IO_TIMING`: default `on`, sets `track_io_timing`.
- `FLUXER_POSTGRES_SHARED_PRELOAD_LIBRARIES`: default `pg_stat_statements`, sets `shared_preload_libraries`.
`shared_preload_libraries` is read only when the server starts. Add a library only if the image ships it, or Postgres refuses to start.
The bundled Valkey uses persistent storage. These settings control its memory limit, its behaviour when full, and how often it writes to disk.
#### `FLUXER_VALKEY_MAXMEMORY`
Default `192mb`. The dataset ceiling. Bounds stored keys only. Client buffers, replication buffers and allocator overhead sit outside it and inside `FLUXER_VALKEY_MEMORY_LIMIT`.
#### `FLUXER_VALKEY_MAXMEMORY_POLICY`
Default `noeviction`. An over-limit write returns an OOM error. Keep this policy to protect deletion queues and other shared state. See [Volumes and buckets](#volumes-and-buckets) for recovery implications.
#### `FLUXER_VALKEY_APPENDFSYNC`
Default `everysec`. How often Valkey flushes its append-only file to disk. `always`, `everysec` or `no`. `everysec` can lose the last second of writes in a crash.
No service sets a CPU limit, a CPU reservation or `cpu_shares`, so every container sees the host's full CPU count. Bound the Gateway's scheduler count with `FLUXER_ERLANG_SCHEDULERS_MIN` and `FLUXER_ERLANG_SCHEDULERS_MAX`, or pin it with `FLUXER_ERLANG_SCHEDULERS` and `FLUXER_ERLANG_DIRTY_CPU_SCHEDULERS` from [Gateway settings](#gateway-settings).
On a host smaller than the 16 GB the defaults assume, lower the large ceilings and the Postgres tuning together. The block below is the 8 GB profile.
```bash
FLUXER_POSTGRES_MEMORY_LIMIT=2560mb
FLUXER_POSTGRES_MEMORY_RESERVATION=1gb
FLUXER_POSTGRES_SHARED_BUFFERS=384MB
FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE=1536MB
FLUXER_API_MEMORY_LIMIT=1280mb
FLUXER_API_MEMORY_RESERVATION=768mb
FLUXER_WORKER_MEMORY_LIMIT=1280mb
FLUXER_WORKER_MEMORY_RESERVATION=768mb
FLUXER_GATEWAY_MEMORY_LIMIT=512mb
FLUXER_GATEWAY_MEMORY_RESERVATION=256mb
FLUXER_MEILISEARCH_MEMORY_LIMIT=1gb
FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=192mb
FLUXER_SEAWEEDFS_MEMORY_LIMIT=1gb
FLUXER_SEAWEEDFS_GOMEMLIMIT=768MiB
FLUXER_GIFS_SHARD_CACHE_MAX_BYTES=134217728
```
At 8 GB the api and worker heap ceilings fall to 664 MB each and Postgres caches less of the working set, which shows up as slower search and slower history scrolling under load. Nothing is turned off, though the largest attachments need the default `2gb` SeaweedFS ceiling to upload every part at once.
The 4 GB profile trades more.
```bash
FLUXER_POSTGRES_MEMORY_LIMIT=1280mb
FLUXER_POSTGRES_MEMORY_RESERVATION=512mb
FLUXER_POSTGRES_SHARED_BUFFERS=192MB
FLUXER_POSTGRES_EFFECTIVE_CACHE_SIZE=768MB
FLUXER_POSTGRES_WORK_MEM=4MB
FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS=120
FLUXER_API_MEMORY_LIMIT=768mb
FLUXER_API_MEMORY_RESERVATION=512mb
FLUXER_WORKER_MEMORY_LIMIT=640mb
FLUXER_WORKER_MEMORY_RESERVATION=384mb
FLUXER_GATEWAY_MEMORY_LIMIT=384mb
FLUXER_GATEWAY_MEMORY_RESERVATION=192mb
FLUXER_MEILISEARCH_MEMORY_LIMIT=1gb
FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=128mb
FLUXER_MEILISEARCH_MAX_INDEXING_THREADS=1
FLUXER_MEDIA_PROXY_MEMORY_LIMIT=320mb
FLUXER_SEAWEEDFS_MEMORY_LIMIT=320mb
FLUXER_SEAWEEDFS_GOMEMLIMIT=240MiB
FLUXER_LIVEKIT_MEMORY_LIMIT=320mb
FLUXER_VALKEY_MEMORY_LIMIT=192mb
FLUXER_VALKEY_MAXMEMORY=128mb
FLUXER_GIFS_SHARD_CACHE_MAX_BYTES=67108864
```
At 4 GB the api heap ceiling is 396 MB and the worker heap ceiling is 332 MB. That covers chat. A large attachment and a search reindex at the same time exceed those heap ceilings. Media transforms above roughly 20 MB start failing in `media-proxy`, Meilisearch indexes a backlog more slowly, and a busy voice room is the first thing to drop. Keep `FLUXER_POSTGRES_SERVER_MAX_CONNECTIONS` at or above 110.
## Health checks and restarts
Compose reads these, and no Fluxer service does. The health checks decide when a dependent service may start and when Docker marks a container unhealthy. All are optional.
- `FLUXER_RESTART_POLICY`: default `unless-stopped`, for every long-running service. `seaweedfs-init` never restarts.
- `FLUXER_HEALTHCHECK_INTERVAL`: default `10s`, for every check.
- `FLUXER_HEALTHCHECK_TIMEOUT`: default `5s`, for every check.
- `FLUXER_HEALTHCHECK_RETRIES`: default `10`, for `edge`, `postgres`, `valkey`, `nats`, `meilisearch`, `livekit` and `static-proxy`.
- `FLUXER_APP_HEALTHCHECK_RETRIES`: default `30`, for the internal services, `api`, `gateway`, `push` and `admin`.
- `FLUXER_APP_HEALTHCHECK_START_PERIOD`: default `90s`, for `api`, `worker` and `gateway`.
- `FLUXER_SVC_HEALTHCHECK_START_PERIOD`: default `60s`, for the internal services, `push` and `admin`.
- `FLUXER_WORKER_HEALTHCHECK_RETRIES`: default `3`, for `worker`. Its check reads a heartbeat file, and a stale one means the worker is stuck.
- `FLUXER_SEAWEEDFS_HEALTHCHECK_RETRIES`: default `20`, for `seaweedfs`.
- `FLUXER_SEAWEEDFS_HEALTHCHECK_START_PERIOD`: default `60s`, for `seaweedfs`. `api`, `worker` and `media-proxy` wait on `seaweedfs-init`, which waits on this check.
- `FLUXER_SEAWEEDFS_INIT_ATTEMPTS`: default `60`. Compose forwards it from `.env` to `seaweedfs-init`, which checks and creates the buckets that many times, 2 seconds apart, before it gives up. Raise it when `docker compose up` fails on `seaweedfs-init` on a slow disk.
Durations take the Compose form, such as `90s` or `2m`. Raise the retries or start period on a slow host where a service is still starting when its check gives up.
## Routing
The edge is the only HTTP entry point, and every route is served from the one public hostname. It rewrites each path before handing it to an upstream. [What the single port routes](/operator/reverse-proxy/#what-the-single-port-routes) has the path table, and [What every proxy must do](/operator/reverse-proxy/#what-every-proxy-must-do) has the requirements for anything in front of the edge.
`FLUXER_ADMIN_BASE_PATH` moves the admin path, and no other variable on this page changes that routing. The `Caddyfile` is a bind mount, so an edit to it takes `docker compose restart edge`.
CORS origins are exactly the app endpoint, the `FLUXER_APP_ORIGIN_ALIASES` origins, and the marketing endpoint. Serving the client from a hostname other than `FLUXER_DOMAIN` requires overriding `FLUXER_APP_ENDPOINT`.
## Volumes and buckets
| Volume | Holds | Back up |
| --- | --- | --- |
| postgres-data | Every account, message, and configuration row | Yes |
| seaweedfs-data | Every uploaded file | Yes |
| valkey-data | Pending file deletions, the thread auto archive schedule, other queues and shared cache | Yes |
| nats-data | Pending and failed background jobs | Yes |
| edge-data | Issued TLS certificates | Optional, a loss only costs a re-issue |
| edge-config | The edge's own state | No |
| meilisearch-data | The search index, rebuildable from Postgres | Optional |
Losing `valkey-data` can delay scheduled account and bulk-message deletions while their queues are rebuilt. Pending asset deletions and cache purges exist only here and are lost with it, so do not treat this volume as disposable cache. The thread auto archive schedule exists only here too. After a loss, an active thread is archived automatically again only once it is modified or unarchived. [Thread lifecycle](/http-api/threads/#thread-lifecycle) states the archive rule.
Losing `meilisearch-data` leaves search empty until it is rebuilt. Nothing rebuilds it automatically, because Postgres still records every channel as indexed. Rebuild each index with [`POST /admin/search/indexes/{index_name}/refreshes`](/admin-api/search-indexes/). The `channel_messages`, `guild_members` and `threads` indexes rebuild one community at a time and need a `guild_id` for each. A backup of the volume avoids those calls on a large instance.
`nats-data` retains pending jobs in `JOBS` for up to 7 days and failed jobs in `JOBS_DLQ` for up to 30 days. A full jobs stream rejects new work. A full dead-letter stream drops its oldest entries, so investigate failures promptly. If dead-letter storage is unavailable, failed jobs remain in `JOBS` only until they expire. Losing this volume loses queued work, which is not automatically recovered from the database.
Startup refuses an existing `JOBS` or `JOBS_DLQ` stream with the wrong name, subjects, retention or storage type, or with sealing, no-ack, mirroring, sources, republishing or a subject transform set. Startup never recreates a stream. When the `JOBS` limits differ, startup updates them in place, unless tightening them would drop queued jobs. Stop publishers and workers before migrating an incompatible stream.
Volume names are prefixed with the Compose project name, so `postgres-data` is `fluxer_postgres-data` on the host.
`seaweedfs-init` creates these buckets and exits. Each bucket takes its name from its variable in `.env`, and the table gives the default.
| Bucket | Set by | Holds |
| --- | --- | --- |
| fluxer | `FLUXER_S3_BUCKET_CDN` | Avatars, guild and entity assets, themes, entrance sounds, memes, and processed attachments |
| fluxer-uploads | `FLUXER_S3_BUCKET_UPLOADS` | Raw attachment uploads, before processing |
| fluxer-reports | `FLUXER_S3_BUCKET_REPORTS` | Abuse report evidence |
| fluxer-harvests | `FLUXER_S3_BUCKET_HARVESTS` | User and guild data archives |
`FLUXER_S3_BUCKET_STATIC` names an optional bucket. The shipped stack does not use or create it.