Skip to content
Fluxer API

Configuration

Fluxer reads its settings from environment variables. They live in a file named .env, in the same directory as docker-compose.yml.

For the bundled Compose stack, start with Core identity and public address and Secrets you must generate. The remaining sections cover custom settings and external services.

The installer in Get started writes .env for you and fills in every secret. Upgrading covers moving between releases.

Compose passes only variables listed in docker-compose.yml. For an unlisted setting, add it to the service’s environment through a local Compose override. Adding it to .env alone has no effect.

Single-quote values containing a literal $ so Compose does not expand them as variable references:

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.
  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. Leave an unwanted override unset, since an empty value does not always restore the default.

FLUXER_DOMAIN is required. Everything else here is optional.

VariableValue in .env.exampleControls
FLUXER_DOMAINchat.example.comThe hostname users type. Reaches services as FLUXER_BASE_DOMAIN. Compose refuses to start when it is unset or empty
FLUXER_PUBLIC_SCHEMEhttpsThe scheme users see. Must be http or https. Anything else fails startup
FLUXER_PUBLIC_PORT443The 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 has it.

Default http. The scheme for internal service URLs. Must be http or https, and anything else fails startup.

Every value below ships as CHANGE_ME. Replace them all before the first start. Compose refuses to start when one is unset or empty, and names the variable. Generate each with openssl rand -hex 32, except the relay secret and the VAPID pair, whose own entries name the command. The installer in Get started generates them all, so come here to rotate one value later or to set them up by hand.

The database login, reused as FLUXER_POSTGRES_PASSWORD. Requires changing the stored role password too.

The Meilisearch master key, reused as FLUXER_SEARCH_API_KEY. Recreate meilisearch and every service that searches.

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.

Sudo mode JWTs, as a raw HS256 key. Changing it ends every active sudo mode session.

Connection initiation tokens and harvest download links. Changing it invalidates connection initiation tokens not yet verified and harvest download links already issued.

Internal RPC between the API and the Gateway. Must be byte-identical on api, worker, and gateway.

Media Proxy URLs. Must match across api, worker, media-proxy, gifs, and unfurl.

FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64

Section titled “FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SECRET_BASE64”

Generate with openssl rand -base64 32. Upload relay capability tokens. Must be standard base64 decoding to at least 32 bytes. Hex fails. Must match on api, worker, and media-proxy. The API and the Media Proxy both check it at boot and refuse to start without it in upload mode.

Admin sessions, CSRF tokens, and OAuth state. Changing it signs every admin out. The admin service refuses to start when it is empty.

The admin OAuth2 client secret. The API requires a non-empty value to serve the admin application.

The BEAM distribution secret. Only the Gateway reads it. Rotate it on every Gateway node at once when clustering.

LiveKit access tokens. Must be at least 32 characters.

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.

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.

.env.example fluxer. The object-storage access key. Reaches services as FLUXER_S3_ACCESS_KEY_ID, which config validation requires to be non-empty.

.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

Section titled “Endpoint derivation from FLUXER_BASE_DOMAIN”

Fluxer builds its public endpoints from the scheme, the base domain, and the port. The docs endpoint is a fixed value. A port of 443 under https or wss, or 80 under http or ws, is omitted.

EndpointDerived value
api, api_clientscheme, base domain, optional port, /api
appscheme, base domain, optional port
gatewayws or wss, base domain, optional port, /gateway
mediascheme, base domain, optional port, /media
static_cdnscheme, base domain, optional port. With FLUXER_STATIC_CDN_DOMAIN set it becomes https:// and that domain, with no port
adminscheme, base domain, optional port, /admin
docsAlways https://fluxer.dev. Not built from the domain settings
marketingscheme, base domain, optional port, /marketing
invitescheme, FLUXER_INVITE_DOMAIN or the base domain, optional port, /invite
giftscheme, 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.

FLUXER_PUBLIC_ORIGIN states the public address as one string, with no trailing slash: the scheme, the host, and the port when that port is not the default for the scheme. FLUXER_PUBLIC_SCHEME, FLUXER_DOMAIN and FLUXER_PUBLIC_PORT state the same address between them, so the spellings have to agree.

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.

Leave it unset unless you need to state the full address explicitly. Compose otherwise derives the public URLs from the scheme, domain and port, including non-default ports.

If you set it, keep these settings consistent with it. A mismatch can break sign-in, setup, passkeys and media access.

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.

Default empty. A separate host for static assets. When set, the static endpoint is forced to https with no port.

Default empty. The host used in invite links. A hostname with no scheme and no path.

Default empty. The host used in gift links. A hostname with no scheme and no path.

Defaults to the derived endpoint. The public API base. The admin service reads the same name as its internal API target, which is why Compose sets it to http://api:8080 for that container only.

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.

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.

Defaults to the derived endpoint. The public Gateway WebSocket URL. Also the source of the internal Gateway URL when FLUXER_INTERNAL_GATEWAY_ENDPOINT is unset, with ws rewritten to http.

Defaults to the derived endpoint. The public Media Proxy URL. The gifs service accepts it as a fallback for FLUXER_MEDIA_PROXY_PUBLIC_ENDPOINT.

Defaults to the derived endpoint. The static asset origin. Read by api, worker, admin, app-proxy, and unfurl.

Defaults to the derived endpoint. The admin origin. The API accepts exactly this value plus /oauth2_callback as the admin OAuth redirect URI.

The admin dashboard sets its cookie flags from the scheme of this value, which Compose builds from FLUXER_PUBLIC_SCHEME. Over HTTPS its CSRF cookie is named __Host-csrf_token and has Secure. Over HTTP it is named csrf_token without Secure.

Defaults to https://fluxer.dev. The docs origin. Never handed to a client.

Defaults to the derived endpoint. The marketing origin. The second allowed CORS origin. Its hostname is extracted and matched.

Defaults to the derived endpoint. The invite base. The API reads links on its hostname as invite links and does not unfurl them.

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.

Default http://127.0.0.1:8080. The API address used by worker and gateway. Compose sets http://api:8080.

No default. The Gateway address the API calls. Falls back to the public Gateway URL with the scheme rewritten. Compose sets http://gateway:8080.

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.

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.

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.

Falls back to FLUXER_STATIC_CDN_ENDPOINT. The static origin used by unfurl. Read only by unfurl.

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 follow FLUXER_PUBLIC_PORT, so a non-default public port moves them with it.

Put your own reverse proxy in front by adding docker-compose.proxy.yml, a second Compose file that overrides parts of the first. Load it with -f twice, or set COMPOSE_FILE=docker-compose.yml:docker-compose.proxy.yml in .env once. The edge then serves plain HTTP on one port, and the proxy in front terminates TLS. Behind your own reverse proxy has the steps to enable the overlay, the requirements, and the per-proxy configuration.

All are optional.

Defaults to the address Compose builds from FLUXER_PUBLIC_SCHEME, FLUXER_DOMAIN and FLUXER_PUBLIC_PORT. What the edge listens on, and the default keeps the listener on the address the instance advertises. Honoured in the bundled layout only, because 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, because a bare hostname means automatic HTTPS on 443 whatever FLUXER_PUBLIC_SCHEME says.

Default private_ranges. The peer addresses whose X-Forwarded-For header the edge trusts. The default is 192.168.0.0/16, 172.16.0.0/12, 10.0.0.0/8, 127.0.0.1/8, fd00::/8 and ::1, which covers every same-host proxy. Narrow it to the proxy’s own address when the proxy reaches the instance from a public address, or when clients reach the proxy from a range the default already covers.

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.

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 port FLUXER_PUBLIC_PORT already names, because that publishes one host port twice and the edge refuses to start. Not read under the overlay, which publishes FLUXER_EDGE_BIND instead.

Defaults to FLUXER_PUBLIC_PORT. 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.

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.

The edge resolves one client address per request and rewrites X-Forwarded-For to it on every upstream hop, so no service reads what a visitor sent. When the peer is outside FLUXER_EDGE_TRUSTED_PROXIES, the edge uses the peer address and discards the header. When the peer is inside the list, the edge takes the rightmost header entry that is not itself trusted. A proxy that appends to the header therefore still passes on the real client address. A client whose own address is inside the list resolves to the proxy, which is why Trusted proxies tells a LAN or VPN deployment to narrow it.

All are optional.

Default false. Whether the client-IP header is trusted. Compose sets true for every service that merges the shared environment block, which app-proxy does not. The API has no peer-address fallback, so false on api makes every non-exempt request a 403.

Default x-forwarded-for. Which header has the client IP. Read by api, worker, admin, app-proxy, and gateway. It must match what the edge sets.

No default. Alias for FLUXER_CLIENT_IP_HEADER_NAME. Accepted only by admin and app-proxy.

CLIENT_IP_HEADER_NAME and CLIENT_IP_HEADER

Section titled “CLIENT_IP_HEADER_NAME and CLIENT_IP_HEADER”

No default. Legacy unprefixed aliases. Accepted only by admin and app-proxy, last in the preference chain.

Default empty. Addresses exempt from IP bans. Comma separated. Every entry must parse as an IP or the API fails at boot.

The API returns 403 for any request whose client-IP header is missing, empty, or not a parsable address, and for every request while FLUXER_TRUST_CLIENT_IP_HEADER is false. 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.

These pick which container images Compose pulls. All are optional.

VariableValue in .env.example
FLUXER_REGISTRY_OWNERfluxerapp
FLUXER_REGISTRYghcr.io/${FLUXER_REGISTRY_OWNER}
FLUXER_IMAGE_TAGv1

FLUXER_REGISTRY_OWNER is the owner segment of the image names and falls back to fluxerapp. FLUXER_REGISTRY is the registry images are pulled from and falls back to ghcr.io/ followed by the owner. FLUXER_IMAGE_TAG is the tag on every Fluxer image and falls back to v1.

These affect only the Fluxer images. docker-compose.yml pins the caddy, postgres, valkey, nats, meilisearch, seaweedfs, and livekit images, and .env cannot change them.

FLUXER_POSTGRES_HOST, FLUXER_POSTGRES_DATABASE, FLUXER_POSTGRES_USERNAME, and FLUXER_POSTGRES_PASSWORD are required in production. The rest are optional.

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.

Default empty. A full connection URL. When set, the discrete host, database, user, and password production checks are skipped.

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.

Default 5432. The database port. Integer 1 to 65535.

Default fluxer. The database name. Must be non-empty.

Default fluxer. The role. Must be non-empty.

Default fluxer. The password. The literal fluxer is rejected in production. CHANGE_ME is not.

Default false. TLS to the database. Must be true or false. In production it must be true unless FLUXER_SELF_HOSTED=true.

Default empty. A CA certificate in PEM form. Used only when SSL is on.

Default 20. Pool size. Integer 1 to 1000, per process. The total is the sum across every container. Compose sets 25 for api and worker and 20 for the database-backed shards.

Default fluxer_kv. The key-value table name. Must match a safe Postgres identifier or startup fails.

Default true. Named prepared statements. Must be true or false. Compose passes 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 and does not include a Cassandra or Scylla service.

Default 127.0.0.1. Contact points. Comma separated. The Rust services normalise each entry to host and port, with IPv6 bracketed.

Default 9042. The port. Integer.

Default fluxer. The keyspace. Must exist already.

Default datacenter1. The local datacentre. Read by api and worker only. The Rust services ignore it.

Default empty. The login. Empty means no authentication.

Default empty. The password. Empty means no authentication.

The bundled stack requires Valkey for shared cache, live updates and deletion queues. General background jobs use NATS JetStream. Change these settings only when customising the cache or connecting an external Redis-compatible service.

Default redis://localhost:6379/0. The Redis-compatible service address. The admin service defaults to empty instead. Compose sets redis://valkey:6379/0.

Default standalone. Use standalone for a single Valkey server or cluster for a Redis-compatible cluster.

Default redis, which is the only accepted value.

Default 30000. Soft cache lifetime in the internal services. Milliseconds. The users and messages routers ignore this setting because they keep no configurable response cache. Their shard caches still honour it.

Default 600000. Hard cache lifetime. Clamped to at least the soft TTL.

Default 100000. Cache entry ceiling. Per process. The users and messages routers ignore this setting because they keep no configurable response cache. Their shard caches still honour it.

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.

Default 300000. How often the API refreshes its IP-ban cache. A non-finite value or one at or below zero disables the timer.

Keep persistent storage and the bundled noeviction policy to protect deletion queues and other shared state. Back up valkey-data. See Volumes and buckets for recovery implications.

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.

Default http://localhost:3900. The S3 API address. media-proxy defaults to empty instead.

No default. The host substituted into presigned URLs, which are the only place it appears.

Default false. Path-style addressing. media-proxy defaults to true instead. Compose sets true.

Default local. The region string. media-proxy defaults to us-east-1. Compose sets us-east-1.

Default empty. The access key. Config validation requires it non-empty on api and worker. Optional for media-proxy.

Default empty. The secret key. Config validation requires it non-empty on api and worker. Optional for media-proxy.

Default empty. A temporary session token. Read by media-proxy only.

Default fluxer. Processed assets. media-proxy defaults to cdn.

Default fluxer-uploads. Raw uploads. media-proxy defaults to uploads, app-proxy to fluxer-uploads.

Default fluxer-downloads. Desktop build artefacts. Read by api and worker.

Default fluxer-reports. Abuse report evidence. Read by api and worker.

Default fluxer-harvests. Data archives. Read by api and worker.

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.

A separate downloads provider is available. FLUXER_S3_DOWNLOADS_ENDPOINT, FLUXER_S3_DOWNLOADS_PUBLIC_ENDPOINT, FLUXER_S3_DOWNLOADS_FORCE_PATH_STYLE, FLUXER_S3_DOWNLOADS_REGION, FLUXER_S3_DOWNLOADS_ACCESS_KEY_ID, and FLUXER_S3_DOWNLOADS_SECRET_ACCESS_KEY take effect only when FLUXER_S3_DOWNLOADS_ENDPOINT is non-empty, and they then replace the primary configuration for the downloads bucket.

The Media Proxy read path has overrides of its own. All are optional.

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.

Falls back to FLUXER_S3_BUCKET_CDN. The read-side bucket. Read by media-proxy only.

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.

Default false. Whether read requests are signed. Read by media-proxy only. Compose sets true, because seaweedfs-init installs an S3 identity and the store then refuses anonymous reads.

Compose sets AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, and AWS_EC2_METADATA_DISABLED for the AWS SDKs. No Fluxer code reads them directly.

The bundled stack provides Meilisearch for message search. These settings select and connect the search service.

Default elasticsearch. Which engine is used. elasticsearch or meilisearch. Compose sets meilisearch.

Default http://127.0.0.1:9200. The engine address. Compose sets http://meilisearch:7700.

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.

Default empty. Basic auth user. Elasticsearch only. Ignored under Meilisearch.

Default empty. Basic auth password. Elasticsearch only.

Default true. Certificate verification. Elasticsearch only. Never passed to the Meilisearch client.

The bundled stack requires NATS for communication between services and JetStream for background jobs. Compose supplies the connection settings. Change them when using an external NATS service, which must have JetStream enabled for the worker connection.

Default nats://127.0.0.1:4222. The core NATS address for api, worker, and gateway. The Gateway defaults to nats://nats:4222 instead.

Alias of FLUXER_NATS_URL on api and worker, read only when that name is unset. The core NATS address. Not read by the Gateway.

Default nats://127.0.0.1:4222. The JetStream address for api and worker. Compose uses FLUXER_NATS_URL unless this setting is supplied separately.

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.

Default nats://127.0.0.1:4222. The NATS address for the internal services. A separate variable from FLUXER_NATS_URL.

No default. Where the Gateway calls the API. Read by the Gateway.

Compose configures the internal services. Change the following settings only when customising their deployment.

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.

Default router. Must be router or shard. Any other value prevents startup.

Default 1. How many shards exist. Fixed at 1 in the shipped stack.

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.

Default 0.0.0.0. The bind address. Serves health and metrics only.

Default 8090. The health and metrics port. Not published.

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 FLUXER_SVC_MAX_CONCURRENT_REQUESTS to users, users-shard, messages, and messages-shard at a default of 20, which pairs with their 20-connection Postgres pools, and leaves the other containers on the built-in defaults.

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.

For custom service routing, add these settings to the API container environment. The bundled Compose file does not forward them:

  • Snowflake service: FLUXER_SNOWFLAKE_SERVICE_SUBJECT and FLUXER_SNOWFLAKE_SERVICE_NATS_CLIENT_NAME.
  • Users service: FLUXER_USERS_SERVICE_SUBJECT and FLUXER_USERS_SERVICE_NATS_CLIENT_NAME.
  • GIF service: FLUXER_GIF_SERVICE_SUBJECT and FLUXER_GIF_SERVICE_NATS_CLIENT_NAME.

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_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.

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.

Default false. The master switch for all voice and video. Compose sets true.

Default empty. The LiveKit API key. Compose fills it from LIVEKIT_API_KEY.

Default empty. The LiveKit secret. Compose fills it from LIVEKIT_API_SECRET.

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. 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.

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.

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.

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.

Default stun1.l.google.com:19302. The second STUN server, used the same way.

Default empty. The server-side LiveKit control API. Compose sets http://livekit:7880.

Default empty. Where LiveKit posts webhooks. The API accepts requests on this route without user authentication and without a client-IP header.

No default. The default voice region. JSON with id, name, emoji, latitude, and longitude.

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.

Default 7882. The UDP media port, published and passed to LiveKit as rtc.udp_port the same way.

LiveKit media does not go through the edge. Compose publishes both media ports directly, so both must stay open in the host firewall and be forwarded to the host when it sits behind NAT. Compose points LiveKit at the webhook target http://api:8080/webhooks/livekit and configures no TURN server. 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.

The Caddyfile is a bind mount, so the edge reads the copy that sits on disk beside docker-compose.yml. Editing it takes docker compose restart edge, because docker compose up -d leaves a container alone when only a mounted file changed. An upgrade does that restart itself, which What the script does covers. A change to any LiveKit value in .env needs docker compose up -d, because restart reuses the existing container with its old environment.

Voice reconciliation has moved from worker to the separate recon service. The old FLUXER_API_WORKER_ENABLE_VOICE_RECONCILIATION and FLUXER_API_WORKER_VOICE_RECONCILIATION_* settings no longer control it. The bundled Compose stack does not start the recon service.

Email is off by default, and the conditions below turn it on. The email switch must be on. It is the value saved in the admin dashboard, or FLUXER_EMAIL_ENABLED when the dashboard has no saved value. Also, the provider must be smtp with a complete SMTP configuration, meaning FLUXER_EMAIL_FROM_EMAIL, FLUXER_EMAIL_SMTP_HOST, FLUXER_EMAIL_SMTP_PORT, FLUXER_EMAIL_SMTP_USERNAME, and FLUXER_EMAIL_SMTP_PASSWORD are all non-empty. All are optional.

The same conditions turn on a DNS check at registration. The check runs when email is on by the rule above. A switch or SMTP value saved in the admin dashboard wins over the matching variable. The address domain has to publish an MX record, or an A or AAAA record as a fallback, and 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, so an owner address at a .lan, .internal or home.arpa name needs email left off.

.env.example false. The delivery switch. The admin dashboard value wins over this.

.env.example none. The transport. smtp or none. Anything else fails startup.

.env.example noreply@example.com. The sender address. Compose falls back to noreply@localhost.

.env.example Fluxer. The sender name. An empty value sets an empty sender name.

.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.

.env.example empty. The SMTP host. Setting any SMTP variable creates the whole SMTP block, which is absent by default.

.env.example 587. The SMTP port. Falls back to 587.

.env.example empty. The SMTP login. Part of the completeness check.

.env.example empty. The SMTP password. Part of the completeness check.

.env.example true. Implicit TLS. Defaults to true whenever the provider is smtp.

The API reads FLUXER_EMAIL_WEBHOOK_SECRET for inbound delivery webhooks. Neither .env.example nor docker-compose.yml has it.

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.

Only api and worker build the mail service, so recreating those two is enough after a change.

All are optional.

VariableValue in .env.exampleControls
FLUXER_CAPTCHA_ENABLEDfalseThe CAPTCHA switch. Startup fails when it is true without a provider and that provider’s keys
FLUXER_CAPTCHA_PROVIDERnoneThe provider. hcaptcha, turnstile, or none. Anything else fails startup

FLUXER_CAPTCHA_HCAPTCHA_SITE_KEY, FLUXER_CAPTCHA_HCAPTCHA_SECRET_KEY, FLUXER_CAPTCHA_TURNSTILE_SITE_KEY, and FLUXER_CAPTCHA_TURNSTILE_SECRET_KEY ship empty in .env.example and the shipped Compose file forwards them all. Set the pair the selected provider needs. The admin dashboard’s Runtime Integrations panel sets the same provider and keys, and a value stored there wins over the environment. An instance that configures CAPTCHA only there must leave FLUXER_CAPTCHA_ENABLED at false, because the boot check reads the environment alone.

All are optional.

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.

Defaults to FLUXER_BASE_DOMAIN. The WebAuthn relying party identifier. Changing it invalidates every passkey already registered.

Default Fluxer. The relying party name browsers display. Does not follow FLUXER_DOMAIN.

The complete accepted origin set, comma separated. Despite the name, an explicit value replaces the defaults and must list every origin clients use. Compose defaults it to the public web app origin. When the variable is omitted outside Compose, the API accepts the configured app origin alongside the built-in Fluxer web and Android origins. An empty list selects only the configured app origin.

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.

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.

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. Add them to the API container environment. The shipped Compose file does not forward them.

FLUXER_VAPID_PUBLIC_KEY and FLUXER_VAPID_PRIVATE_KEY are required. FLUXER_VAPID_EMAIL is optional.

VariableValue in .env.exampleControls
FLUXER_VAPID_PUBLIC_KEYCHANGE_METhe VAPID public key. Base64url of the 65-byte uncompressed P-256 point
FLUXER_VAPID_PRIVATE_KEYCHANGE_METhe VAPID private key. Base64url of the 32-byte scalar, and the matching half of the pair
FLUXER_VAPID_EMAILunsetThe VAPID contact address. Compose derives admin@ followed by FLUXER_DOMAIN when it is unset

The Gateway reads the same names. A malformed pair, or a private key that does not derive the public point, does not stop it. It records the fault in its log at startup and then drops every web push notification.

FLUXER_GATEWAY_PUSH_ENABLED is read by the Gateway alone, defaults to true, and skips that startup check on the VAPID pair when it is false.

Both api and gateway read the APNs and FCM names. None appear in .env.example or in docker-compose.yml, so configuring mobile push means editing the Compose file. All are optional.

Default false. The APNs switch. Must be set on both api and gateway.

No default. The Apple team. Paired with the key ID.

No default. The signing key ID. Paired with the team ID.

No default. The signing key in PEM form. Set exactly one of this and the path.

No default. A path to the signing key. Must be readable inside the container.

Default production. Which APNs environment is used. production or development.

Default []. Per-app APNs configuration. JSON array. An entry with no app_id fails startup.

Default false. The FCM switch. Must be set on both api and gateway.

No default. The Firebase project. Paired with the client email.

No default. The service account address. Paired with the project.

No default. The service account key. Set one of this, FLUXER_PUSH_FCM_PRIVATE_KEY_PATH, or FLUXER_PUSH_FCM_SERVICE_ACCOUNT_JSON_PATH.

No default. A path to the key. Must be readable inside the container.

No default. A path to the whole service account JSON. Must be readable inside the container.

Default https://oauth2.googleapis.com/token. The OAuth token endpoint. Change only for a proxy or a test double.

Default []. Per-app FCM configuration. JSON array, under the same app_id rule as APNs.

Stripe billing, which the shipped stack keeps off. All are optional.

Default false. The Stripe switch. Compose hardcodes false, so a self-hosted instance cannot enable it from .env.

Default empty. The Stripe secret key. Not forwarded by the shipped Compose file.

Default empty. The webhook signing secret. Not forwarded by the shipped Compose file.

Default {}. Every price ID at once. JSON object. An individual price variable overrides the matching price in this 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, NOK, PLN, SEK, and TRY, plus FLUXER_STRIPE_PRICE_VISIONARY_ and FLUXER_STRIPE_PRICE_GIFT_VISIONARY_ in USD and EUR.

Default {}. Retired price IDs, keyed by the same slot names FLUXER_STRIPE_PRICES uses, each mapped to a list: {"monthly_brl": ["price_..."]}. JSON object. Existing subscriptions on these prices keep renewing. The localised checkout catalogue uses FLUXER_STRIPE_PRICES alone.

Repricing a slot has a fixed order. Doing the steps in another order leaves a price ID unknown to the API, and invoices on that price fail. 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 as soon as the signature verifies and hands the event to worker, so any retry comes 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 for as long as any subscription still bills on it, which for a yearly price is at least a year after the switch.

All are optional.

Default false. NCMEC reporting. Compose hardcodes false.

Default empty. The reporting endpoint. Required when reporting is on.

Default empty. The reporting login. Required when reporting is on.

Default empty. The reporting password. Required when reporting is on.

Default empty. The contact address on reports. Required when reporting is on.

Default false. Upload virus scanning. Compose hardcodes false, and no ClamAV container ships.

Default 127.0.0.1. The scanner host. Needs a reachable scanner.

Default 3310. The scanner port. Integer.

Default false. Behaviour when the scanner is unreachable. With scanning on and this off, an unreachable scanner rejects every upload.

FLUXER_API_CONTENT_MODERATION_NSFW_THRESHOLD

Section titled “FLUXER_API_CONTENT_MODERATION_NSFW_THRESHOLD”

Default 0.7. The API-side NSFW score cutoff. No range check. Distinct from the Media Proxy threshold, which defaults to 0.85.

Default false. IP intelligence. Needs an ipinfo key to do anything.

Default empty. The ipinfo key. Paired with FLUXER_RISK_INTEGRATION_ENABLED.

No default. The account risk policy. JSON. Malformed JSON fails startup, and an unknown key in a well-formed policy surfaces when the policy runs.

Default false. Whether every Tor relay is blocked. Covers entry and middle relays as well as exit nodes.

Default false. Reverse DNS Tor detection. Adds a lookup to the request path.

Default 750. The lookup timeout. Milliseconds.

Default empty. Allowed inbound phone countries. Comma separated, passed through unvalidated.

FLUXER_ABUSE_PHONE_INBOUND_REQUIRED_PREFIXES

Section titled “FLUXER_ABUSE_PHONE_INBOUND_REQUIRED_PREFIXES”

Default empty. Required inbound prefixes. Comma separated.

Default false. Direct contact spam detection.

FLUXER_ABUSE_DIRECT_CONTACT_SPAM_COUNTRY_CODES

Section titled “FLUXER_ABUSE_DIRECT_CONTACT_SPAM_COUNTRY_CODES”

Default empty. Countries the rule applies to. Comma separated, unvalidated.

FLUXER_ABUSE_DIRECT_CONTACT_SPAM_DISTINCT_TARGET_THRESHOLD

Section titled “FLUXER_ABUSE_DIRECT_CONTACT_SPAM_DISTINCT_TARGET_THRESHOLD”

Default 25. Distinct targets before the rule fires. Integer.

FLUXER_ABUSE_DIRECT_CONTACT_SPAM_TARGET_WINDOW_MS

Section titled “FLUXER_ABUSE_DIRECT_CONTACT_SPAM_TARGET_WINDOW_MS”

Default 7200000. The observation window. Milliseconds, two hours by default.

Default flag_spammer. What happens when it fires. flag_spammer or suppress_delivery. Anything else fails startup.

Defaults to the inverse of FLUXER_SELF_HOSTED. External blocklist feeds. Off by default on a self-hosted instance.

A second family, unrelated to the rules above, tunes the IP auto-banner: FLUXER_ABUSE_WINDOW_MS, the FLUXER_ABUSE_THRESHOLD_ names, the FLUXER_ABUSE_TOKEN_DIVERSITY_ names, FLUXER_ABUSE_BAN_TTL_SEC, FLUXER_ABUSE_BATCH_FLUSH_MS, FLUXER_ABUSE_MAX_BATCH_TICKS, FLUXER_ABUSE_MAX_NEW_TOKENS_PER_TICK, FLUXER_ABUSE_MAX_TRACKED_IPS, FLUXER_ABUSE_MAX_TOKEN_HASHES_PER_IP, FLUXER_ABUSE_MIN_SCORE_FOR_LOOKUP, FLUXER_ABUSE_MIN_TOKENS_FOR_LOOKUP, and FLUXER_ABUSE_REQUIRED_SCORE_WINDOWS_FOR_AUTO_BAN. All are read directly from the environment, none are in .env.example or the Compose file, and a non-finite value or one at or below zero falls back to the default.

Other names in that family limit how often the auto-banner buys an IP classification from ipinfo. A shared claim lets one replica do the lookup for the others. With the claim off, every API replica looks up the same attacking IP at the same moment, so one IP costs one lookup per replica.

  • FLUXER_ABUSE_IP_CLASS_CLAIM_ENABLED defaults to 1 and gates the shared claim. It is read as a string, and only 0 turns the claim off.
  • FLUXER_ABUSE_IP_CLASS_CLAIM_TTL_SEC defaults to 15 seconds and sets how long a replica holds that claim.
  • FLUXER_ABUSE_IP_CLASS_PENDING_TTL_MS defaults to 20000 milliseconds and sets how long a replica that lost the claim waits before it tries again.
  • FLUXER_ABUSE_IP_CLASS_NEGATIVE_TTL_MS defaults to 300000 milliseconds and sets how long a failed classification is remembered.
  • FLUXER_ABUSE_IP_CLASS_HINT_TTL_MS defaults to 600000 milliseconds and sets how long a class sent by another replica stays usable.

For these numeric settings, a non-finite value or one at or below zero falls back to the default.

A FLUXER_IPINFO_BUDGET_ family caps what the instance spends at ipinfo. FLUXER_IPINFO_BUDGET_ENABLED defaults to 1, and 0 turns off all shedding. FLUXER_IPINFO_BUDGET_MONTHLY_MAX defaults to 140000 and is the ceiling for one UTC calendar month. Lookups run at the priorities below, each with a share of that ceiling and a token bucket for bursts refilled once a minute.

  • Admin IP bans and scheduled deletion checks are critical. They reach the full ceiling, with a burst of 60 from FLUXER_IPINFO_BUDGET_CRITICAL_BURST refilled at 60 a minute by FLUXER_IPINFO_BUDGET_CRITICAL_REFILL_PER_MIN.
  • Registration risk is standard. It stops at FLUXER_IPINFO_BUDGET_STANDARD_MONTHLY_PCT percent of the ceiling, default 90, with a burst of 240 from FLUXER_IPINFO_BUDGET_STANDARD_BURST refilled at 120 a minute by FLUXER_IPINFO_BUDGET_STANDARD_REFILL_PER_MIN.
  • The IP auto-banner is background. It stops at FLUXER_IPINFO_BUDGET_BACKGROUND_MONTHLY_PCT percent, default 60, with a burst of 120 from FLUXER_IPINFO_BUDGET_BACKGROUND_BURST refilled at 30 a minute by FLUXER_IPINFO_BUDGET_BACKGROUND_REFILL_PER_MIN.

Background lookups stop first and critical lookups stop last. A denied lookup returns an unavailable result. If the key-value store fails, lookups remain allowed at every priority, so an outage can increase usage beyond these budgets.

Local MaxMind data can reduce registration-risk lookups. Set FLUXER_RISK_IPINFO_PRESCREEN_ENABLED to 1 or true, in any letter case, and fill FLUXER_RISK_IPINFO_PRESCREEN_ALLOW_ASNS with comma-separated ASN numbers. Non-numeric entries are dropped. An IP skips ipinfo only when MaxMind supplies its country and an allowed ASN, and the organisation is not a commercial privacy provider, education network or cellular network.

Use the admin dashboard or Admin instance API 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.
  • 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.

No environment variable changes an instance limit. Use the admin dashboard’s Limit Config page or the Admin API. Effective limits combine saved settings, deployment defaults and premium policy. After a change is saved, every running api and worker process reloads the limits with no restart. Clients receive the limit configuration object.

Request concurrency is separate from instance limits, and each process sets its own. All are optional.

Default 512. The API in-flight ceiling. Integer 1 to 100000, checked at startup.

Default 512. Gateway HTTP RPC concurrency.

Default 512. Gateway NATS handler count.

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.

Default false. Loosens registration rate limits. Reaches production containers if set.

All are optional.

Defaults to debug in development, info otherwise. The Node log level. Read by api and worker.

Default info. The Rust log filter. Read by media-proxy, app-proxy, admin, and the internal services. Not in .env.example or the Compose file.

Default info. The Gateway log level. Compose sets info.

Falls back to FLUXER_GATEWAY_LOGGER_LEVEL. The Gateway log level at runtime. Overrides the prefixed name.

Default dev. The reported build. Baked into the images. When BUILD_VERSION is unset or empty outside development, the process prints a warning.

Default stable. The reported channel. Only canary is recognised as non-stable.

Defaults to false at the Gateway’s environment layer. Gateway telemetry. Read only by the Gateway, and in neither .env.example nor the Compose file.

Set by Docker. Node identity in metrics and logs. Also used by the IP auto-banner.

Default development. The runtime mode. development, production, or test. Compose pins production, which gates Postgres validation and the admin cookie flags.

Default false. The self-host switch. Compose sets true. It relaxes the production Postgres SSL requirement, seeds the limit tier, gates registration, billing and discovery controllers, and turns blocklist feeds off.

/_metrics on api, media-proxy, and gateway, plus the Gateway’s /_health/ready, /_health/drain, and /_health/undrain, are gated to loopback peers, so no proxy reaches them. The probes that work from outside are /api/_health, /gateway/_health, /media/_health, and the edge’s own /_health.

All are optional.

Default true. The public guild discovery surface. With it off, discovery search, discovery join, and the guild discovery application routes return 400 DISCOVERY_DISABLED.

Default 1. Minimum members for discovery eligibility. Integer.

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

Section titled “FLUXER_API_PRESIGNED_ATTACHMENT_UPLOADS_ENABLED”

Default false. Presigned attachment uploads. Compose sets true.

Default false. Presigned downloads. Applies to the downloads bucket.

FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED

Section titled “FLUXER_API_PRESIGNED_HARVEST_DOWNLOADS_ENABLED”

Default true. Presigned harvest downloads. Applies to the harvests bucket.

Default false. oEmbed HTML in embeds.

FLUXER_API_EMBEDS_OEMBED_HTML_ALLOW_UNTRUSTED_ON_SELF_HOSTED

Section titled “FLUXER_API_EMBEDS_OEMBED_HTML_ALLOW_UNTRUSTED_ON_SELF_HOSTED”

Default false. Untrusted oEmbed HTML on a self-hosted instance. Security relevant. Leave it off.

FLUXER_API_EMBEDS_OEMBED_HTML_ALLOWED_HOSTS

Section titled “FLUXER_API_EMBEDS_OEMBED_HTML_ALLOWED_HOSTS”

Default empty. Hosts allowed to supply oEmbed HTML. Comma separated.

FLUXER_API_EMBEDS_CACHE_DEFAULT_TTL_SECONDS

Section titled “FLUXER_API_EMBEDS_CACHE_DEFAULT_TTL_SECONDS”

Default 86400. Default embed cache lifetime. Seconds.

Default 604800. Maximum embed cache lifetime. Seconds.

Default 300. Minimum embed cache lifetime. Seconds.

FLUXER_API_EMBEDS_CACHE_RESPECT_REMOTE_TTL

Section titled “FLUXER_API_EMBEDS_CACHE_RESPECT_REMOTE_TTL”

Default true. Whether a remote cache header is honoured. Clamped by FLUXER_API_EMBEDS_CACHE_MIN_TTL_SECONDS and FLUXER_API_EMBEDS_CACHE_MAX_TTL_SECONDS.

Default empty. Hosts never unfurled. Comma separated, unvalidated.

FLUXER_API_DESKTOP_GITHUB_REDIRECT_COUNTRIES

Section titled “FLUXER_API_DESKTOP_GITHUB_REDIRECT_COUNTRIES”

Default empty. Countries redirected to GitHub for desktop downloads. Each entry must be two uppercase letters or the API fails at boot.

Default false. Test mode. Collapses the deletion grace period. Reaches production containers if set.

No default. The test harness credential. Reaches production containers if set.

Defaults to on outside production. Response schema validation. Costs latency when forced on.

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.

Default empty. The YouTube Data API key. Also read by unfurl, which accepts YOUTUBE_API_KEY as a fallback.

Default none. How cached copies of deleted or replaced media are purged. none or http. Anything else fails startup. With none, nothing is queued or sent.

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 {"exact": [...], "prefix": [...]} here every 10 seconds. Redirects are not followed. Any 2xx marks the batch done. Your endpoint then purges those URLs from every proxy that caches media. A 400 or 422 makes the worker resend each URL alone, and a URL refused alone moves to the cache_purge:rejected key. Any other failure is retried on a later run.

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.

Default 10000. Purge request timeout. Accepts 1000 to 10000. The range is checked only when the adapter is http.

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. app-proxy also accepts MAXMIND_DB_PATH. The API also reads an ASN database. On a filesystem path it looks for GeoLite2-ASN.mmdb in the same directory. On an s3:// URL it needs an asn_key query parameter, and takes an optional absolute asn_download_path next to it. A missing ASN database is not an error, and the API falls back to ipinfo for ASN data.

None of these are in .env.example or in docker-compose.yml. Set branding from the admin dashboard instead. All are optional.

Default Fluxer. The product name clients display. Also settable in the admin dashboard, which wins.

Default empty. The client icon. Its origin is added to the CSP by app-proxy.

Default empty. The symbol mark. Its origin is added to the CSP by app-proxy.

Default empty. The logo. Its origin is added to the CSP by app-proxy.

Default empty. The wordmark. Its origin is added to the CSP by app-proxy.

Default empty. The favicon. Its origin is added to the CSP by app-proxy.

Default empty. The theme colour.

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.

Default empty. An invite every new account joins. Must be a live invite code.

Default empty. The guild that grants the visionary role. Snowflake.

FLUXER_VISIONARIES_GUILD_VISIONARY_ROLE_ID

Section titled “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 unauthenticated setup access and first-registration admin grant described in Get started.

FLUXER_API_WORKER_TASK is required under single_task. The rest are optional.

Default 8080. The API listen port. Compose sets 8080 and the edge proxies to it.

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, so raising it alone does nothing.

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.

Default all_lanes. Which lanes the worker runs. all_lanes, single_lane, or single_task. Anything else fails startup.

No default. Which lane, under single_lane. realtime, unfurl, lifecycle, or batch.

No default. Which task, under single_task. Must name a known task.

No default. Whether the cron scheduler runs. Compose sets true. Without it no scheduled job runs anywhere.

FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES

Section titled “FLUXER_API_WORKER_LANE_CONCURRENCY_OVERRIDES”

No default. Per-lane concurrency. JSON object keyed by lane. 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.

The Media Proxy takes uploads, transforms images, and serves media back. All are optional.

Default mp. Which routes are served. mp, static, or upload. Anything else is a startup error. Compose sets upload.

Default 0.0.0.0. The bind address. Also settable with --bind-host.

Default 8080. The listen port. Also settable with --port.

Default false. Refuses writes. --read-only can force it on.

Default local. Where objects live. local or s3. Compose sets s3.

Default ./media_proxy_storage. The local storage directory. Used only by the local backend.

FLUXER_MEDIA_PROXY_UPLOAD_RELAY_MAX_BODY_BYTES

Section titled “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

Section titled “FLUXER_MEDIA_PROXY_UPLOAD_RELAY_TOKEN_TTL_SECS”

Default 900. Relay token lifetime. Read by the API alone.

Default http://localhost:8088/media. The relay URL handed to clients. Node side only. A trailing slash and a trailing /v1/relay are stripped.

FLUXER_MEDIA_PROXY_UPLOAD_RELAY_KEEP_DIRECT_COUNTRIES

Section titled “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

Section titled “FLUXER_MEDIA_PROXY_UPLOAD_RELAY_S3_TIMEOUT_MS”

Default 900000. Relay upload timeout. Accepts 1000 to 3600000.

Defaults to the system temporary directory. Where relay bodies spool. Must be writable.

FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_CHUNK_BYTES

Section titled “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

Section titled “FLUXER_MEDIA_PROXY_UPLOAD_RELAY_SPOOL_MAX_TOTAL_BYTES”

Default 8589934592. Total spool ceiling. 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.

Defaults to available parallelism, clamped to 2 to 8. Concurrent image transforms. Accepts 1 to 128.

Defaults to eight times the transform limit. Transform queue depth. Accepts 1 to 8192.

Default 268435456. Transform cache size. Accepts 0 to 4 GiB.

FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES

Section titled “FLUXER_MEDIA_PROXY_TRANSFORM_CACHE_MAX_ENTRY_BYTES”

Default 67108864. Largest cacheable result. Accepts 0 to 512 MiB.

Default 120000. Transform cache lifetime. Accepts 0 to 3600000.

Default 30000. Socket read and write timeout. Accepts 0 to 300000.

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.

Default 15000. Per-transform timeout. Accepts 1000 to 120000.

Default 20000. Animation frame ceiling. Accepts 1 to 100000.

Default 30000. Animation duration ceiling. Accepts 100 to 600000.

Default empty. An external NSFW classifier. No such service ships with the stack.

Default 0.85. The media-side NSFW cutoff. Accepts 0.0 to 1.0 and finite. Distinct from the API threshold.

media-proxy range-checks these values at startup and then reads them nowhere: FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_BYTES and FLUXER_MEDIA_PROXY_UPLOAD_RELAY_BUFFERED_RETRY_TOTAL_BYTES. A bad value still fails the boot.

The Gateway is the WebSocket service clients hold open for live events. All are optional.

Default 8771. The listen port. Compose sets 8080.

Default all. Which subsystems this node runs. websocket, sessions, presence, guilds, calls, push, or all. An unrecognised value also becomes all.

Default http://localhost:8088/media. The public media URL used in payloads. Compose builds it from the public origin.

Default http://localhost:8088. The public static origin used in payloads. Compose builds it from the public origin.

FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES

Section titled “FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_ENTRIES”

Default 128. Presence push buffer depth.

FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES

Section titled “FLUXER_GATEWAY_PRESENCE_PUSH_BUFFER_MAX_BYTES”

Default 1048576. Presence push buffer size.

Default 5000. Milliseconds. The Gateway parses it into its configuration at startup, and nothing reads it after that, so it controls no drain.

Default 6. Failures before the API circuit opens. Integer.

Default 15000. How long the circuit stays open. Milliseconds.

Default false. BEAM clustering. Single-node by default.

No default. The DNS name peers are discovered through. Needs clustering on.

FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME

Section titled “FLUXER_GATEWAY_CLUSTER_DISCOVERY_NODE_BASENAME”

No default. The node basename for discovery. Needs clustering on.

FLUXER_GATEWAY_CLUSTER_DISCOVERY_POLL_INTERVAL_MS

Section titled “FLUXER_GATEWAY_CLUSTER_DISCOVERY_POLL_INTERVAL_MS”

Default 5000. Discovery poll interval. Milliseconds.

Default empty. A fixed peer list. Comma separated Erlang node names, capped at 256. An entry that is not a node name fails startup.

Default fluxer_gateway@127.0.0.1. The BEAM node name. Must be resolvable by peers when clustering.

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.

Default 8081. The BEAM distribution port. Not published by Compose. Never expose it.

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.

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.

Default 2. The floor of the scheduler clamp. Compose forwards it to gateway.

Default 16. The ceiling of the scheduler clamp. Compose forwards it to gateway, and .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.

The clamp bounds reach the Gateway from .env. To pin the count, set FLUXER_ERLANG_SCHEDULERS on the gateway service in docker-compose.yml, which 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 has the close code.

The Gateway reads every FLUXER_GATEWAY_ name straight from the environment, so any of them works once it is in the gateway container environment. The bundled Compose file passes only the names it lists, so add any other name through a Compose override file.

app-proxy serves the web client. All are optional.

Default 0.0.0.0. The bind address. Compose sets it explicitly.

Default 8080. The listen port. Compose sets it explicitly.

Default ./static. Where the client’s SPA bundle lives, unrelated to static-proxy.

No default. An upstream to fetch index.html from. Leave unset for the shipped image.

Default http://localhost:8088/api/.well-known/fluxer. Where the bootstrap discovery document is fetched. 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.

Default 60000. How often discovery is refetched. Unprefixed name.

Default /api. The API path put in the page bootstrap.

No default. The absolute API URL in the bootstrap. Compose builds it from the public origin.

Defaults to the inverse of FLUXER_SELF_HOSTED. A frozen asset snapshot. Also compiled out of the self-hosted image.

admin serves the dashboard at /admin. All are optional.

Default 0.0.0.0. The bind address. Compose sets it explicitly.

Default 3020. The listen port. The image sets 8080 and Compose sets 8080.

Defaults to empty in Rust, /admin in Node. The path the dashboard is mounted under. Normalised to a leading slash with no trailing slash. The service serves at root and re-prefixes every emitted URL with this.

Defaults to the admin endpoint plus /oauth2_callback. The OAuth2 redirect. Must equal what the API derives from FLUXER_ADMIN_ENDPOINT. The API does not read this name.

Default stable. The reported channel. RELEASE_CHANNEL is preferred over it.

Defaults to the crate version. The reported build. BUILD_VERSION is preferred over it.

app-proxy builds a per-request nonce-based policy for the client HTML and the assets it serves. Each variable appends sources to one directive on top of the built-in ones. All are empty by default, and Compose forwards every one.

Every name below goes in .env. app-proxy reads its environment at container start, so a change takes effect on docker compose up -d app-proxy. A docker compose restart app-proxy does not apply it.

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 reads the discovery document and adds the static CDN endpoint, the media endpoint, and the origins of the configured branding images. A stack on one hostname therefore 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 has that line in place.

A front proxy must not add a Content-Security-Policy of its own.

Keys in .env.example that no service reads

Section titled “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.

Interpolated into FLUXER_BASE_DOMAIN and into the derived URL strings.

Read by Docker Compose to select the proxy overlay.

Read by the edge container only, and only in the bundled layout. The overlay overwrites it with :8080.

Read by Docker Compose as the default for FLUXER_EDGE_SITE_ADDRESS when that name is unset.

Read by the edge container only. It takes effect on docker compose up -d edge.

Used as the host side of the overlay’s port mapping.

Used as the host side of the edge’s published ports.

FLUXER_REGISTRY_OWNER, FLUXER_REGISTRY and FLUXER_IMAGE_TAG

Section titled “FLUXER_REGISTRY_OWNER, FLUXER_REGISTRY and FLUXER_IMAGE_TAG”

Image name selection.

Becomes FLUXER_POSTGRES_PASSWORD and the Postgres image’s own password.

Becomes FLUXER_SEARCH_API_KEY and the Meilisearch image’s own key.

FLUXER_S3_ACCESS_KEY and FLUXER_S3_SECRET_KEY

Section titled “FLUXER_S3_ACCESS_KEY and FLUXER_S3_SECRET_KEY”

Become FLUXER_S3_ACCESS_KEY_ID and FLUXER_S3_SECRET_ACCESS_KEY, and the AWS_ pair.

Become FLUXER_LIVEKIT_API_KEY and FLUXER_LIVEKIT_API_SECRET, and LiveKit’s own LIVEKIT_KEYS.

.env.example lists the variables forwarded by docker-compose.yml. Add any setting below through a Compose override file that puts it in the relevant service’s environment.

FLUXER_AUTH_BLUESKY_ and the names under it

Section titled “FLUXER_AUTH_BLUESKY_ and the names under it”

See Bluesky connections for configuration.

Mobile push cannot be configured at all from the example.

The only way to change log verbosity.

FLUXER_APP_PRODUCT_NAME and the branding URLs

Section titled “FLUXER_APP_PRODUCT_NAME and the branding URLs”

Branding is otherwise admin-dashboard only.

Supplies the initial setup state on a self-hosted instance, until the first write of the stored app_public_config row takes over.

Every FLUXER_ABUSE_ auto-banner name, every FLUXER_IPINFO_ budget name, and every Media Proxy performance knob

Section titled “Every FLUXER_ABUSE_ auto-banner name, every FLUXER_IPINFO_ budget name, and every Media Proxy performance knob”

No example coverage at all.

The admin dashboard at /admin stores these in the database. They apply without recreating containers, and a value set here wins over the matching environment variable.

Product name, client-visible brand assets, and the setup state in the instance discovery document.

Registration mode of open, approval, or closed, admin-issued registration URLs, and pending approval requests.

Single-community mode, direct messages and friends, the premium model, and optional embed services.

The Klipy GIF key, the YouTube Data API key, the CAPTCHA provider and its keys, email delivery with an SMTP connection test, and Bluesky OAuth.

Size-based attachment lifetimes. The built-in default is on.

Instance Config, Gateway Rollout Configuration

Section titled “Instance Config, Gateway Rollout Configuration”

Session and guild rollout percentages, RPC timeouts, and Gateway concurrency.

OIDC-style SSO for the client and admin apps, and whether SSO is enforced.

The instance limits published to clients.

The voice regions offered and the servers behind them.

Credentials for the Admin API.

Changes propagate without a restart. A process that cannot apply an update logs the error and keeps its previous valid settings.

These settings exist only in the dashboard: attachment decay, which defaults to on, and the inactivity deletion threshold, which defaults to 365 days.

The stack runs its containers on one Docker bridge network, which is private to the stack.

ServiceImageWhat it does
edgecaddy:2.10-alpineTLS and path routing, the only HTTP entry point
app-proxyfluxer-app-proxy-self-hostedServes the web client and builds its CSP header
static-proxyfluxer-staticServes the static asset bundle
apifluxer-apiThe HTTP API
workerfluxer-apiBackground lanes and the cron scheduler
gatewayfluxer-gatewayThe Gateway WebSocket
media-proxyfluxer-media-proxyUploads, transforms, and media delivery
adminfluxer-adminThe admin dashboard
snowflakes, snowflakes-shardfluxer-snowflakesIdentifier allocation
users, users-shardfluxer-usersUser reads and writes
messages, messages-shardfluxer-messagesMessage reads and writes
gifs, gifs-shardfluxer-gifsGIF provider access
unfurl, unfurl-shardfluxer-unfurlLink unfurling
postgrespostgres:16-alpineThe database
valkeyvalkey/valkey:8.1-alpineThe key-value store and pub/sub bus, and the deletion queues
natsnats:2.14-alpineCore messaging, and the JetStream streams holding queued background jobs
meilisearchgetmeili/meilisearch:v1.12The search index
seaweedfschrislusf/seaweedfs:4.34S3-compatible object storage
seaweedfs-initchrislusf/seaweedfs:4.34Creates the buckets and the S3 identity, then exits
livekitlivekit/livekit-server:v1.12.0Voice and video

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.

api is the one service an operator configures directly, through the shared environment block. worker, gateway, app-proxy, and media-proxy are touched rarely, worker for lane concurrency, app-proxy for CSP extras, and media-proxy for transform limits. The edge takes only the FLUXER_EDGE_ variables, postgres only the password, meilisearch only the master key, valkey only the FLUXER_VALKEY_ tuning values, and livekit only the key pair and the port variables. static-proxy reads no environment variables, and the remaining services need none.

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.

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 16.75 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.

Default 256mb. The ceiling for edge.

Default 5gb. The ceiling for postgres. Must be at or above FLUXER_POSTGRES_MEMORY_RESERVATION or the container fails to create. The 256mb shm_size is charged against it.

Default 256mb. The ceiling for valkey. Must stay above FLUXER_VALKEY_MAXMEMORY, which bounds the stored dataset alone.

Default 256mb. The ceiling for nats. Covers JetStream file store metadata as well as the core server.

Default 768mb. The ceiling for meilisearch. Must stay well above FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY, which bounds the indexer alone.

Default 384mb. Becomes MEILI_MAX_INDEXING_MEMORY. The memory the indexer may use for one batch. Lower it whenever you lower FLUXER_MEILISEARCH_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.

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.

Default 128mb. The ceiling for seaweedfs-init. A one-shot container that exits, so it never overlaps steady state.

Default 512mb. The ceiling for livekit. Grows with the number of concurrent voice publishers.

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.

Default 2560mb. The ceiling for worker. Same Node derivation as api, applied to every background lane at once.

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.

Default 512mb. The ceiling for media-proxy. Image and video transforms decode into this ceiling, so a large upload is what reaches this limit.

Default 256mb. The ceiling for static-proxy. The service reads no environment variables and serves files only.

Default 256mb. The ceiling for app-proxy. It holds the discovery cache and reads no database.

Default 128mb. The ceiling for snowflakes. A router holds no shard state.

Default 256mb. The ceiling for snowflakes-shard. Holds the identifier buffer sized by FLUXER_SNOWFLAKE_SERVICE_BATCH_SIZE.

Default 128mb. The ceiling for users. A router holds no shard state.

Default 256mb. The ceiling for users-shard. Holds the read cache bounded by FLUXER_SVC_CACHE_MAX_ENTRIES, which defaults to 100000 entries.

Default 128mb. The ceiling for gifs. A router holds no shard state.

Default 256mb. The ceiling for gifs-shard. Lower than the 536870912 byte default of FLUXER_GIFS_SHARD_CACHE_MAX_BYTES.

Default 128mb. The ceiling for messages. A router holds no shard state.

Default 256mb. The ceiling for messages-shard. The shard has a Postgres pool of 20 connections alongside the read cache.

Default 128mb. The ceiling for unfurl. A router holds no shard state.

Default 256mb. The ceiling for unfurl-shard. Fetches remote pages, so a slow upstream holds bytes for the length of the fetch.

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.

Default 3gb. The reclaim floor for postgres. Must be at or below FLUXER_POSTGRES_MEMORY_LIMIT. Lowering the limit alone fails container creation.

Default 1gb. The reclaim floor for api. Must be at or below FLUXER_API_MEMORY_LIMIT.

Default 1gb. The reclaim floor for worker. Must be at or below FLUXER_WORKER_MEMORY_LIMIT.

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.

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.

No default. The V8 old-space ceiling for worker, in MB. Same rule against FLUXER_WORKER_MEMORY_LIMIT.

The names on the Postgres command line tune the bundled server. Keep them consistent with FLUXER_POSTGRES_MEMORY_LIMIT. All are optional.

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.

Default 512MB. The shared buffer pool. Allocated at server start, so it is charged to the container whether or not it is used.

Default 2GB. The planner’s assumption about disk cache. Allocates nothing. Lowering it makes the planner prefer sequential scans and frees no memory.

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.

Default 256MB. The budget for one VACUUM, CREATE INDEX, or ALTER TABLE. One such operation at a time normally holds it.

Default 128MB. The budget for each autovacuum worker. Postgres runs three workers by default, so budget three times this value.

The remaining Postgres settings are fixed on the command line, with no variable of their own: min_wal_size=512MB, max_wal_size=2GB, wal_buffers=16MB, and shm_size: 256mb on the service itself.

The bundled Valkey uses persistent storage. These settings control its memory limit and behaviour when full.

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.

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 for recovery implications.

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.

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.

Terminal window
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=512mb
FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=256mb
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.

Terminal window
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=384mb
FLUXER_MEILISEARCH_MAX_INDEXING_MEMORY=128mb
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.

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 has the path table, and What every proxy must do has the requirements for anything in front of the edge.

None of the variables on this page change that routing. The Caddyfile is a bind mount, so an edit to it takes docker compose restart edge.

CORS origins are exactly the app and marketing endpoints. Serving the client from a hostname other than FLUXER_DOMAIN requires overriding FLUXER_APP_ENDPOINT.

VolumeHoldsBack up
postgres-dataEvery account, message, and configuration rowYes
seaweedfs-dataEvery uploaded fileYes
valkey-dataDeletion queues and shared cacheYes
nats-dataPending and failed background jobsYes
edge-dataIssued TLS certificatesOptional, a loss only costs a re-issue
edge-configThe edge’s own stateNo
meilisearch-dataThe search index, rebuildableNo

Losing valkey-data can delay scheduled account and bulk-message deletions while their queues are rebuilt. Pending asset deletions and cache purges can be lost, so do not treat this volume as disposable cache.

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.

BucketHolds
fluxerAvatars, guild and entity assets, themes, entrance sounds, memes, and processed attachments
fluxer-uploadsRaw attachment uploads, before processing
fluxer-downloadsDesktop client build artefacts
fluxer-reportsAbuse report evidence, and NCMEC payloads where that integration is on
fluxer-harvestsUser and guild data archives

FLUXER_S3_BUCKET_STATIC names an optional bucket. The shipped stack does not use or create it.