Chapter 4 · Observe and operate
Going to production
A production FluxGate is pinned images, a managed database, two secrets, TLS in front, an edge near your apps, single sign-on and a JWT secret you can rotate.
Before you start
- You control a container host with access to Docker Hub, and FluxGate is not installed there yet.
- You have a managed PostgreSQL database, a reverse proxy or load balancer that holds your TLS certificate, and an OpenID Connect provider.
- You tried FluxGate on the demo stack from Install and first boot. This page changes the demo stack into something you can run for a team.
- For the UI steps, sign in as
adminSystem admin and select the team Checkout.
The production checklist
The demo stack proves that FluxGate works. Production needs decisions: which versions run, where secrets live, and who terminates TLS.
- Pinned images. Use
forgeopslabs/flux-gate-backend:v2.0.1,forgeopslabs/flux-gate-edge:v2.0.1andforgeopslabs/flux-gate-ui:v2.0.1. A moving tag such aslatestcan change under you on a restart. Upgrade by editing the tag. - Managed database. Point the backend at your PostgreSQL with
DATABASE_URL. Publish no PostgreSQL port. The backend applies its database migrations at startup, so back up the database before each upgrade. - One backend instance. Run one backend. Scale the edge server, not the backend.
- Two secrets in a secret store.
FLUXGATE_ENCRYPTION_KEYis required.TYPESAFE_API_KEYis optional. Keep both out of the compose file. -
config.tomlmounted. Setallowed_origin,http_addr,grpc_addrandpublic_base_url. - TLS at a reverse proxy. Terminate TLS with your own certificate. Keep the gRPC port private.
- An edge server near your apps. Run it in the same network or cluster as the apps that evaluate flags, with a snapshot directory so it survives a backend outage. See Edge resilience.
- Single sign-on with your OpenID Connect provider, tested before you enforce it.
- A JWT secret you know how to rotate, and the emergency action you hope never to use.
- Approval policy and roles. Give each person only the roles they need. See Approvals and policies.
The secrets
Two environment variables carry secrets.
FLUXGATE_ENCRYPTION_KEY is the base64 of 32 random bytes. It encrypts the secrets that FluxGate stores, such as single sign-on client secrets and Jira tokens. If the key changes, the stored secrets stop decrypting and you must enter them again. Generate it once, and back it up in your secret manager:
openssl rand -base64 32TYPESAFE_API_KEY is optional. It only turns on the Jev AI features. See Jev AI assistance.
Your platform injects both values into the container environment from its secret store. The example below reads them from the environment of the host that runs docker compose, so nothing secret sits in the file.
The compose file
# Example only: adapt names, addresses and secret handling to your platform.
services:
backend:
image: forgeopslabs/flux-gate-backend:v2.0.1
restart: unless-stopped
environment:
DATABASE_URL: ${DATABASE_URL:?set DATABASE_URL}
FLUXGATE_ENCRYPTION_KEY: ${FLUXGATE_ENCRYPTION_KEY:?set FLUXGATE_ENCRYPTION_KEY}
# Optional: turns on the Jev AI features
TYPESAFE_API_KEY: ${TYPESAFE_API_KEY:-}
volumes:
- ./config.toml:/app/config/config.toml:ro
ports:
# REST API: reach it through your reverse proxy only
- "127.0.0.1:8080:8080"
# gRPC on 50051 is not published: only edge servers use it
ui:
image: forgeopslabs/flux-gate-ui:v2.0.1
restart: unless-stopped
environment:
# The browser calls the backend directly: use its public address
BACKEND_HOST: fluxgate-api.example.com
BACKEND_PORT: "443"
BACKEND_PROTOCOL: https
WS_PROTOCOL: wss
ports:
- "127.0.0.1:3000:80"
depends_on:
- backend
edge:
image: forgeopslabs/flux-gate-edge:v2.0.1
restart: unless-stopped
environment:
EDGE_BACKEND_GRPC: ${EDGE_BACKEND_GRPC:?set EDGE_BACKEND_GRPC}
EDGE_HTTP_ADDR: 0.0.0.0:8081
EDGE_CLIENT_ID: ${EDGE_CLIENT_ID:?set EDGE_CLIENT_ID}
EDGE_CLIENT_SECRET: ${EDGE_CLIENT_SECRET:?set EDGE_CLIENT_SECRET}
ports:
# POST /evaluate on the edge takes no credentials, so publish 8081 on
# the loopback address only. Reach it through your proxy, or replace
# 127.0.0.1 with a private address that only your apps can reach.
- "127.0.0.1:8081:8081"What differs from the demo file:
| Setting | Demo | Production |
|---|---|---|
| Images | v2.0.1 | v2.0.1, pinned on purpose |
| Database | A postgres service with a published port | DATABASE_URL points at your managed PostgreSQL, with sslmode=require |
| Secrets | A .env file next to the compose file | Injected from a secret store |
| Backend port 8080 | Published on all interfaces | Published on 127.0.0.1 for the proxy |
| gRPC port 50051 | Inside the compose network | Never published; only edge servers use it |
| UI variables | localhost, 8080, http, ws | The public HTTPS address of the backend, 443, https, wss |
| Edge server | Behind the edge profile, port 8081 on all interfaces | A normal service with required credentials, port 8081 on 127.0.0.1 |
DATABASE_URL has the form postgres://<user>:<password>@<host>:5432/<database>?sslmode=require. URL-encode special characters in the password: @ becomes %40, / becomes %2F and : becomes %3A. Run the backend as a single instance.
Mount a config.toml next to the compose file. The backend reads it from /app/config/config.toml in the container:
# The UI's origin, exactly as the browser shows it
allowed_origin = "https://fluxgate.example.com"
# Where the REST and gRPC servers listen inside the container
http_addr = "0.0.0.0:8080"
grpc_addr = "0.0.0.0:50051"
# The backend's public address; single sign-on callbacks are built from it
public_base_url = "https://fluxgate-api.example.com"allowed_originis the exact HTTPS address of your UI. The backend checks the origin of browser requests against it.http_addrandgrpc_addrare the listeners inside the container.public_base_urlis the public HTTPS address of the backend. Single sign-on callbacks are built from it. A trailing slash is removed.
Without a mounted file the backend falls back to a default config, so always mount yours.
Check the file
Run docker compose config to check the file before you start anything. The command does not start containers. Give it dummy values for the required variables:
DATABASE_URL=postgres://fluxgate:dummy@db.internal:5432/feature_toggle?sslmode=require \
FLUXGATE_ENCRYPTION_KEY=ZHVtbXk= \
EDGE_BACKEND_GRPC=http://backend:50051 \
EDGE_CLIENT_ID=00000000-0000-0000-0000-000000000000 \
EDGE_CLIENT_SECRET=dummy \
docker compose -f docker-compose.production.yml config -qNo output and exit code 0 mean the file is valid. Without the variables, compose stops with required variable DATABASE_URL is missing a value: set DATABASE_URL, which is what you want on a host that forgot a secret.
TLS and the reverse proxy
Terminate TLS at a reverse proxy or load balancer that you own, with your certificate. Two public addresses are common: one for the UI, such as https://fluxgate.example.com, and one for the backend, such as https://fluxgate-api.example.com. The browser calls the backend directly, so the UI container variables point at the backend's public HTTPS address.
Configure the proxy as follows:
- Forward cookies on
/api/v1/auth/sso/. The state cookiefluxgate_sso_statehas that path. A proxy that strips it breaks every login withsso_state_invalid. - Pass WebSocket upgrades for
/api/v1/ws. The UI opens a WebSocket there. - Set the
ForwardedandX-Forwarded-*headers and strip values that clients send. Withpublic_base_urlset, FluxGate builds callbacks from it and does not need them for that purpose. - Keep the gRPC port 50051 on the private network. Only edge servers need it.
Browser --TLS--> proxy --> UI (3000) and backend REST (8080)
Edge server --gRPC--> backend (50051), inside the private networkThe edge server near your apps
Run the edge server in the same network or cluster as the apps that evaluate flags, so evaluations stay local. It needs four settings: EDGE_BACKEND_GRPC, EDGE_HTTP_ADDR, EDGE_CLIENT_ID and EDGE_CLIENT_SECRET. All four are required.
The client credentials come from a client that you create in the UI. EDGE_CLIENT_ID is the client's UUID, not the SDK key. EDGE_CLIENT_SECRET is its API key. See Clients and edge server.
Size the edge server
Pick the smallest size whose tested rate covers your peak traffic per edge server. In load tests with 1,000 flags and 10,000 users, each size held these rates for 5 minutes with p99 at or under 50 ms:
| Edge limits | Held in tests, per second | p99 at that rate |
|---|---|---|
| 0.25 CPU, 64 MiB | 2,100 | 1.0 ms |
| 0.5 CPU, 128 MiB | 4,550 | 4.4 ms |
| 1 CPU, 256 MiB | 7,690 | 1.6 ms |
| 2 CPU, 512 MiB | 19,250 (limited by the test host) | 15.6 ms |
CPU grew by about 0.06 cores per 1,000 evaluations per second (fitted line). On the smallest size, setting EDGE_CACHE__ASSIGNMENT_MAX_CAPACITY=10000 kept edge memory near 27 MiB. Above about 2,000 evaluations per second per edge, some evaluation events were dropped, so the dashboards undercount; serving was not affected. No size held more than 19,250 per second per edge in these tests, and setups with several edge servers were not tested. The full results and the test method are on the performance page.
An edge server on another host
When the edge server runs on a different host than the backend, it needs a network route to the gRPC port 50051 of the backend. The example does not publish that port. Publish it only on a private address, for example "10.0.0.5:50051:50051", and allow only your edge hosts to reach it. The gRPC link uses plaintext http://, so expose it only on a private, trusted network. Then change the edge setting:
EDGE_BACKEND_GRPC=http://10.0.0.5:50051Use the address or DNS name of the backend host on that private network.
To keep apps evaluating flags through a backend outage, read Edge resilience. Set a snapshot directory on every edge as a first step.
Set up single sign-on
Single sign-on lets people sign in with your OpenID Connect provider. Single sign-on has the full setup and identity provider recipes. The short version:
- Sign in as
adminand click Single Sign-On in Settings to open/settings/sso, titled "Single sign-on". - Click Add provider. In the dialog Add SSO provider, fill Display name, Slug, Issuer URL, Client ID, Client secret, Groups claim and Role sync mode. Redirect URI shows the callback address.
- Register
<public_base_url>/api/v1/auth/sso/<slug>/callbackat the provider. The match is exact. The dialog can show a port the backend does not send, such as:443, so register the address built frompublic_base_url. - Click Create provider.
- Click the Group mappings icon on the row of the provider. Click Add mapping, type your group in IdP group value, choose Role and the role
Approver, then click Save mappings. The toast "Group mappings saved" appears. Mappings turn identity provider groups into roles, teams or admin access. - Test a login with an ordinary user.
- Only then decide on Enforce single sign-on. It turns off password sign-in, except for break-glass admins that you made by hand. A warning on the page says so. Leave the switch off until a test login works.
Rotate the JWT secret
FluxGate signs authentication tokens with a JWT secret. This is the token the backend issues for UI sessions. It is not a flag token. Two actions on /settings/jwt change it, and they answer different needs.
- Generate New Secret is routine rotation. The new secret signs new tokens. The old secret keeps verifying for a grace period equal to the access-token lifetime (30 minutes by default), so people stay signed in. Use it on a schedule. It is not a response to a leaked secret, because the old secret still works for a while.
- Deactivate All JWT Secrets is the emergency action. It stops every secret at once and signs everyone out. Use it when a secret leaks.
- Click JWT Settings in Settings to open
/settings/jwt, "JWT Secret Management". Current JWT Secrets lists each secret with its ID, a preview, a status and its creation time. Only a preview of the secret shows. - To rotate, click Generate New Secret. The page warns: "Generating a new secret will not invalidate existing tokens until they expire naturally."
- For a leaked secret, scroll to Emergency Actions and click Deactivate All JWT Secrets. The page asks you to confirm. Click Cancel unless you mean it.
Emergency Actions shows only while at least one secret is active. By default, access tokens last 30 minutes and refresh tokens 7 days. These are defaults of the backend configuration.
Good to know
- Run one backend instance. Scale the edge server for more evaluation traffic.
- Back up the database before every upgrade, because the backend applies migrations at startup.
- Use the same address in
public_base_url, in the redirect URI at the identity provider, and in the UI container variables. - Do not change
FLUXGATE_ENCRYPTION_KEYwhile the database exists. See Install and first boot.
Troubleshooting
Browser requests fail with a CORS error
allowed_origin does not match the address in the browser address bar. Set it to the exact HTTPS address of the UI, with no trailing path.
Every single sign-on login fails with sso_state_invalid
The proxy strips the fluxgate_sso_state cookie. Forward cookies on /api/v1/auth/sso/.
The identity provider reports a redirect_uri mismatch
Set public_base_url and register the exact address <public_base_url>/api/v1/auth/sso/<slug>/callback.
The UI loads but live updates fail
The proxy does not pass WebSocket upgrades on /api/v1/ws. Enable them, and check that WS_PROTOCOL is wss.
Next
This is the end of the series. Go back to What is FluxGate, or to the section you are setting up.