Deployment

You are in the right place if you are shipping FluxLit with Docker or Kubernetes, wiring health checks, or choosing between fluxlit dev and fluxlit run.

FluxLit serves one public port: the ASGI gateway (Uvicorn) proxies API traffic to FastAPI and everything else to a Streamlit subprocess. Point browsers and load balancers at that port only; see Architecture for the request path.

Production entrypoint

Use fluxlit run (no reload). Typical container command:

fluxlit run app:app --host 0.0.0.0 --port 8000

The target (module:attr) resolves the same way as fluxlit dev: CLI argument → fluxlit.toml / [tool.fluxlit] targetapp:app. Bind address and port also follow Configuration precedence (CLI → env → project file → defaults).

Behind a reverse proxy, pass --proxy-headers (or set FLUXLIT_TRUST_PROXY=1) and configure FLUXLIT_ROOT_PATH when the app is mounted under a subpath — see the reverse-proxy section in Configuration. For a multi-segment public path such as /apps/my-app, nginx Compose, smoke checks, and TLS/trust notes, see Path-prefixed mount (/apps/my-app) in Production TLS and edge headers and docker/proxy-deployment/docker-compose.apps-prefix.yml in the repository.

Running under Uvicorn directly

The usual pattern matches FastAPI: your FluxLit instance is the ASGI app.

uvicorn app:app --host 0.0.0.0 --port 8000

Set target in fluxlit.toml (or FluxLit(import_target=...), or FLUXLIT_APP) when the import path is not app:app. Set gateway_port in fluxlit.toml (or FLUXLIT_GATEWAY_PORT) to match --port when it is not 8000.

Legacy / factory entrypoint (same stack, requires env):

export FLUXLIT_APP="app:app"
uvicorn fluxlit.runtime:create_unified_app --factory --host 0.0.0.0 --port 8000

Notes:

  • Uvicorn --workers > 1 is not supported for the unified stack; lifespan startup fails if Uvicorn’s worker count is greater than one (unless FLUXLIT_ALLOW_UNIFIED_UVICORN_MULTIWORKER=1, which is explicitly unsupported). Prefer one process per replica — see Scaling and workers below.

  • Lifespan follows the ASGI spec; the inner FastAPI app’s lifespan runs after the Streamlit sidecar starts.

Health checks

Probe

Path

Meaning

Liveness

GET /api/healthz

FastAPI app is up (does not check Streamlit).

Readiness

GET /api/readyz

When the unified runtime has configured a Streamlit upstream (FLUXLIT_STREAMLIT_UPSTREAM), returns 200 only if GET on the upstream root returns 2xx. 503 on connection errors, 5xx/4xx/3xx from the upstream, or missing upstream state. If no upstream is configured (e.g. tests), returns 200 with streamlit: not_configured.

Both routes are hidden from OpenAPI so they do not clutter /api/docs. Use them in Kubernetes livenessProbe / readinessProbe, load balancers, or Compose healthcheck curls.

Example checks:

curl -fsS http://127.0.0.1:8000/api/healthz
curl -fsS http://127.0.0.1:8000/api/readyz

Kubernetes: graceful shutdown

FluxLit runs Uvicorn and a Streamlit child in one pod. When Kubernetes sends SIGTERM, Uvicorn stops accepting new connections and drains in-flight HTTP/WebSockets up to timeout_graceful_shutdown, then runs ASGI lifespan shutdown (which tears down Streamlit). Align timeouts so the pod is not SIGKILL mid-drain.

  • FLUXLIT_UVICORN_GRACEFUL_SHUTDOWN_TIMEOUT_S — optional; forwarded to Uvicorn as timeout_graceful_shutdown. Set it below terminationGracePeriodSeconds, leaving time for preStop hooks and for the runtime’s bounded Streamlit termination (SIGINT / terminate / kill sequence) after lifespan exits.

  • terminationGracePeriodSeconds — must exceed Uvicorn’s graceful window plus any preStop sleep you add for load balancers to stop sending traffic before SIGTERM.

  • preStop — common pattern: sleep 5 (or similar) so endpoints update before the main process sees SIGTERM; alternative is active coordination with your ingress. This is not a substitute for Uvicorn drain; it only reduces in-flight work at cutoff.

Ordering: ingress / kube-proxy stop sending new connections → SIGTERM → Uvicorn drain → lifespan stops Streamlit → process exits. If drain is too long, Kubernetes still SIGKILL after the grace period.

Docker and Compose

  • fluxlit build writes a minimal Dockerfile and .dockerignore into the current directory (or -o / --output). Adjust the generated files for your dependency layout, base image digest, non-root user, and image size. The template uses a digest-pinned python:3.12-slim base, runs as appuser, CMD ["fluxlit", "run", "<target>"], and sets FLUXLIT_GATEWAY_HOST=0.0.0.0. Refresh the FROM python@sha256:… line when you intentionally upgrade the base image (match docker pull python:3.12-slim then inspect RepoDigests).

  • Production images should install from a committed lockfile (pip-tools / uv, etc.) your app controls; fluxlit build stays minimal on purpose.

  • A runnable Compose example lives in the repository at examples/docker_compose/ (requirements.txt from pip-compile, exposes port 8000).

  • For nginx, TLS, and subpath smoke tests, see docker/proxy-deployment/ in the repo.

Do not run fluxlit dev with --reload in production images.

For TLS termination, HSTS, forwarded header trust, and CSP guidance, see Production TLS and edge headers. For secrets, logs, and JWT/OIDC rotation, see Secrets lifecycle.

Reverse proxy compatibility matrix

Runnable smoke stacks live under docker/proxy-deployment/ in the repository (run-all-proxy-smokes.sh runs them sequentially in CI-like automation).

Stack

Public port

Path mode

Edge

WebSocket notes

nginx strip-prefix (default compose)

8080

/myapp/ stripped before FluxLit

nginx 1.27

Upgrade / Connection map; long proxy_read_timeout

nginx root (full URL path)

8082

No strip; origin path matches app

nginx 1.27

Same as strip-prefix

nginx full-path

8081

Proxy passes full browser path

nginx 1.27

Match FLUXLIT_ROOT_PATH / Streamlit baseUrlPath to proxy

nginx /apps/my-app prefix

8083

Multi-segment prefix; see Path-prefixed mount (/apps/my-app) in Production TLS and edge headers

nginx 1.27

Same Upgrade map

HTTPS + nginx

8444

Strip-prefix with TLS

nginx + test certs

Use CURL_INSECURE=1 for self-signed in smoke

Caddy strip-prefix

8084

handle_path /myapp/* → upstream

Caddy 2.8

Third engine; same strip-prefix contract

Traefik strip-prefix

8085

stripPrefix + PathPrefix(/myapp)

Traefik 3.2

File provider; same contract as nginx strip-prefix

Caddy uses docker-compose.caddy.yml merged with the base docker-compose.yml (FluxLit service unchanged). Run:

docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d --build
PUBLIC_PREFIX=/myapp BASE_URL=http://127.0.0.1:8084 ./smoke-test.sh

Traefik uses docker-compose.traefik.yml merged with the base file (strip-prefix via file provider). Run:

docker compose -f docker-compose.yml -f docker-compose.traefik.yml up -d --build
PUBLIC_PREFIX=/myapp BASE_URL=http://127.0.0.1:8085 ./smoke-test.sh

Observability in production

  • Enable FLUXLIT_ENABLE_GATEWAY_ACCESS_LOG=1 only if your log pipeline can handle per-request volume; pair with filters and fluxlit.logging.redact where headers are copied into logs — Observability.

  • For JSON lines (Loki, Datadog, Cloud Logging), attach JsonLogFormatter to your root, uvicorn, and fluxlit loggers (sample dictConfig in Observability).

  • FLUXLIT_ENABLE_REQUEST_LOGGING affects the inner FastAPI app only (not the gateway dispatch line).

Runtime-injected environment

The parent process sets variables for the Streamlit child and for gateway code that reads upstream state. You normally do not set these by hand when using fluxlit run; see Runtime-managed environment variables.

Scaling and workers

Single process (default)

  • One Uvicorn worker is the supported model: one gateway ASGI app and one Streamlit child share loopback and process-local state (upstream URL file, OIDC BFF in-memory stores, etc.).

  • Uvicorn --workers > 1 is not supported for this unified stack in one OS process: extra workers would each try to own a Streamlit subprocess and shared resources would diverge. Do not enable multi-worker on one pod to “use all CPUs”; scale out instead (see below). Starting with recent FluxLit releases, unified ASGI lifespan startup fails fast when Uvicorn’s Config.workers is greater than one (unless you set FLUXLIT_ALLOW_UNIFIED_UVICORN_MULTIWORKER=1, which keeps the layout explicitly unsupported).

Horizontal scale (multiple replicas)

Behind a Layer 7 load balancer or Kubernetes Service with multiple endpoints:

  • Each replica runs its own gateway + Streamlit pair. Streamlit’s default session is tied to the server-side script run and WebSocket; after a hard refresh or new connection, users may land on a different replica and see a new session unless you add affinity.

  • Sticky sessions (session affinity / cookie-based or IP-hash) route the same browser to the same replica for a period. That improves continuity for interactive UIs but is not a full multi-replica session store: long-lived affinity tables, draining nodes, and failures still drop local state.

  • When to add an external session store: if you need consistent application state across replicas without sticky sessions (or in addition to them), persist state outside the process (database, Redis, etc.). FluxLit’s URL-session helpers provide the cookie-free binding pattern; production multi-replica continuity still depends on the store you choose. See URL session continuity (no cookies) for SessionStore recipes and External store recipes.

Multi-replica operations checklist

Use this when replicas > 1 (Kubernetes) or multiple VMs/containers sit behind one load balancer:

Decision

Guidance

Sticky sessions

Prefer cookie-based or connection affinity from your ingress/LB when you need Streamlit’s in-process st.session_state to survive refreshes without an external store. IP-hash is simple but breaks for mobile/NAT and during node drains.

OIDC BFF / in-memory auth

The bundled BFF patterns use process-local state for some flows; multiple replicas require you to externalize state/session or accept affinity-only routing. See Security architecture (auth / BFF sections) and Runbooks (Auth misconfig).

URL-session continuity

InMemorySessionStore is per replica. For cookie-free continuity across replicas, implement a shared SessionStore (Redis, SQL, etc.); see URL session continuity (no cookies).

Rollouts

Keep readiness on /api/readyz, use PodDisruptionBudget during voluntary disruptions, and align preStop + terminationGracePeriodSeconds with Uvicorn drain — see Kubernetes: graceful shutdown and examples/kubernetes/pod-disruption-budget.example.yaml / service-session-affinity.example.yaml.

Observability

Correlate gateway request_id across replicas in your log stack; see Observability.

Rollout and drain playbook

For multi-replica deployments:

  1. Run one FluxLit process per replica. Do not use Uvicorn worker fan-out inside a replica.

  2. Use readiness (/api/readyz) to remove a replica before it receives user traffic.

  3. Give Streamlit WebSockets time to close during rollouts by aligning preStop, terminationGracePeriodSeconds, and FLUXLIT_UVICORN_GRACEFUL_SHUTDOWN_TIMEOUT_S.

  4. If users must survive replica replacement or non-sticky routing, store continuity state in an external SessionStore; in-memory stores are per replica.

  5. Keep sticky sessions as a routing optimization, not as the only persistence layer for important app state.

Supported alternatives to multi-worker

  • One process per replica (Kubernetes Pod, ECS task, VM): scale replica count; tune CPU for a single process.

  • Split topology (advanced): run Streamlit and the API on different hosts and point FLUXLIT_STREAMLIT_UPSTREAM at the Streamlit origin—only if your platform requires separate services and you accept operational complexity.

  • Multiple Uvicorn workers for API-only does not apply to FluxLit unified mode; use plain FastAPI + separate Streamlit hosting if you truly need multi-worker HTTP for the API only.

Ingress engines: affinity, WebSockets, strip-prefix (pointers)

Engine

Affinity / stickiness

WebSockets / /_stcore/stream

Strip-prefix vs full path

nginx

ip_hash or commercial sticky modules; verify your build’s directives.

Pass Upgrade / Connection; raise proxy_read_timeout for long-lived streams.

References: docker/proxy-deployment/nginx.conf, nginx-apps-prefix.conf.

Traefik

Sticky sessions on routers/services (cookie-based); see Traefik version docs.

Treat WebSocket like HTTP Upgrade; avoid buffering middleware on WS routes.

Repo: docker/proxy-deployment/ Traefik merge compose (see commands earlier in this page).

Caddy

reverse_proxy lb_policy / cookie selection (syntax varies by Caddy 2 minor).

Tune flush / timeouts for long streams; see Caddy compose under docker/proxy-deployment/.

Align handle_path / URI rewrites with FLUXLIT_ROOT_PATH.

Always match FLUXLIT_TRUST_PROXY, FLUXLIT_ROOT_PATH, and FLUXLIT_PUBLIC_BASE_URL to what the edge forwards — details in Production TLS and edge headers.

Reference: Kubernetes

A minimal Deployment + Service that matches the hardened image contract (probes, graceful shutdown, non-root) lives in examples/kubernetes/ in the repository. Copy and adjust image name, resources, and ConfigMap / Secret wiring for your cluster.

Checklist

  • [ ] fluxlit doctor passes (or only acceptable WARNs).

  • [ ] Optional CI gate: fluxlit doctor app:app --json is parsed and checked for unexpected FAIL diagnostics.

  • [ ] FLUXLIT_GATEWAY_HOST / bind address matches container/platform (often 0.0.0.0).

  • [ ] Proxy: FLUXLIT_TRUST_PROXY, FLUXLIT_ROOT_PATH, and FLUXLIT_PUBLIC_BASE_URL (for OAuth) set correctly.

  • [ ] Readiness probe uses /api/readyz when Streamlit must be up before receiving traffic.

  • [ ] Optional: set FLUXLIT_UVICORN_GRACEFUL_SHUTDOWN_TIMEOUT_S (and Kubernetes terminationGracePeriodSeconds / preStop) per Kubernetes: graceful shutdown above.

  • [ ] Secrets in env or a secrets manager — not baked into images; .env excluded from Docker context (default .dockerignore from fluxlit build already ignores .env). See Secrets lifecycle.

  • [ ] FLUXLIT_FORWARDED_ALLOW_IPS tightened when FLUXLIT_TRUST_PROXY is on (not * in untrusted networks). See Production TLS and edge headers.

  • [ ] For Kubernetes: start from examples/kubernetes/ and align probes and terminationGracePeriodSeconds with Kubernetes: graceful shutdown.