Environment variables
Every variable the compose stack reads, where each one comes from, and the ones the template forgets.
deploy/platform/.env is the stack's runtime configuration.
bootstrap.sh creates it from .env.example and fills every value that
ends in replace-me; nothing else is generated for you. One rule decides
whether a variable reaches a service: only the gateway is handed the whole
file (env_file), so any ${VAR} referenced from litellm/config.yaml
works by being in .env. Every other service gets an explicit list in
docker-compose.yml, and a variable missing from that list is invisible
to the container however carefully you set it.
Secrets bootstrap generates
| Variable | Generator | Read by |
|---|---|---|
LITELLM_MASTER_KEY | hex 32 | the gateway; the app and the console call it with this key |
LITELLM_SALT_KEY | hex 32 | the gateway, key hashing |
POSTGRES_PASSWORD | hex 16 | Postgres, Langfuse, postgres-exporter |
DATABASE_URL | derived: postgresql://npuops:<password>@postgres:5432/npuops | the gateway |
LANGFUSE_PUBLIC_KEY, LANGFUSE_SECRET_KEY | hex 16 | the gateway, the console, the e2e test |
LANGFUSE_NEXTAUTH_SECRET, LANGFUSE_SALT | base64 32 | Langfuse |
LANGFUSE_ENCRYPTION_KEY | hex 32 | Langfuse |
LANGFUSE_INIT_USER_PASSWORD | hex 12 | Langfuse, the first admin's password |
CLICKHOUSE_PASSWORD, MINIO_ROOT_PASSWORD | hex 16 | ClickHouse, MinIO, Langfuse |
JWT_SECRET, JWT_REFRESH_SECRET | hex 32 | the app and the console; must be identical in both |
CREDS_KEY, CREDS_IV | hex 32, hex 16 | the app, credential encryption |
MONGO_INITDB_ROOT_PASSWORD | hex 16 | MongoDB |
MONGO_URI | derived: mongodb://librechat:<password>@mongodb:27017/LibreChat?authSource=admin | the app |
GRAFANA_ADMIN_PASSWORD | hex 12 | Grafana |
E2E_USER_PASSWORD | hex 16 | the e2e test |
Two consequences of how the fill works. A value you blanked stays blank;
only the literal replace-me suffix is replaced. And once Postgres or
MongoDB has initialised its volume, its password lives in the volume: a
later change in .env breaks the connection instead of rotating the
password. grep -c replace-me .env before the first up on a server.
Set by hand
Infrastructure:
| Variable | Default | Read by |
|---|---|---|
POSTGRES_USER, POSTGRES_DB | npuops, npuops | Postgres, Langfuse, the exporter |
REDIS_HOST, REDIS_PORT, REDIS_PASSWORD | redis, 6379, empty | the gateway, redis-exporter |
CLICKHOUSE_USER, MINIO_ROOT_USER | clickhouse, minio | ClickHouse, MinIO, Langfuse |
LANGFUSE_HOST | http://langfuse-web:3000 | the gateway, where it sends traces |
LANGFUSE_NEXTAUTH_URL | http://localhost:3000 | Langfuse, its own public URL |
LANGFUSE_INIT_USER_EMAIL, LANGFUSE_INIT_USER_NAME | admin@npuops.local, NPUOps Admin | Langfuse, the first admin |
GRAFANA_ADMIN_USER | admin | Grafana |
The app (librechat service):
| Variable | Default | What it does |
|---|---|---|
APP_TITLE | NPUOps | the name in the UI; set it to NuFi |
ALLOW_REGISTRATION, ALLOW_EMAIL_LOGIN | true, true | self sign-up; password sign-in |
CUSTOM_FOOTER, HELP_AND_FAQ_URL, PRIVACY_POLICY_URL, TERMS_OF_SERVICE_URL | © NPUOps, https://npuops.local/docs, empty, empty | footer text and links |
CONSOLE_URL | http://localhost:3001 | the Console entry in the account menu; empty hides it |
LIBRECHAT_URL | http://localhost:3080 | the app's own public URL; the compose file derives DOMAIN_CLIENT and DOMAIN_SERVER from it, and hands it to the console |
Models, read by the gateway:
| Variable | Default | What it does |
|---|---|---|
GPU_BACKEND_BASE_URL, GPU_BACKEND_API_KEY | http://host.docker.internal:11434/v1, ollama | the local or GPU model server add-model.sh registers against |
NPU_BACKEND_BASE_URL, NPU_BACKEND_API_KEY | empty | the NPU server, same shape |
GEMINI_API_KEY | empty, required | litellm/config.yaml ships two Gemini entries and the gateway refuses to start while the key is empty; set it, or remove the entries and rebuild |
any other os.environ/NAME in litellm/config.yaml | add-model.sh --api-key-env NAME adds the reference; you add the value here |
Guardrails and tests:
| Variable | Default | What it does |
|---|---|---|
SCANNER_MODEL_ID, SCANNER_MODEL_REVISION | protectai/deberta-v3-base-prompt-injection-v2, a pinned commit | the injection classifier the scanner downloads |
HF_TOKEN | empty | only if the model needs an authenticated download |
E2E_USER_EMAIL, E2E_USER_NAME, E2E_MODEL, E2E_EXPECTED_HARDWARE_ID, E2E_ENDPOINT_NAME | e2e@npuops.local, E2E Bot, qwen2.5-3b, mac-local, NPUOps | the end-to-end test, which does not pass today; see Troubleshooting |
LAKERA_API_KEY | empty | read by nothing; left over from an earlier control |
Read by the stack, missing from the template
Add these to .env yourself.
| Variable | Fallback | Read by |
|---|---|---|
NUFI_CONSOLE_TAG | main | the console image tag |
DEFAULT_USER_BUDGET, DEFAULT_BUDGET_DURATION, DEFAULT_TPM_LIMIT, DEFAULT_RPM_LIMIT | 10, 30d, 10000, 60 | the console, for a newly provisioned user |
Two more are honoured by the app but never reach it on this stack,
because the librechat service's environment list does not include them.
Until the two lines are added to docker-compose.yml, setting them in
.env does nothing:
| Variable | What it would do |
|---|---|
AGENTS_URL | librechat.yaml uses it for the Agents entry that leads to NuFi Studio and NuFi Works; without it the entry is hidden |
COOKIE_DOMAIN, COOKIE_SAMESITE | share the session across subdomains; see SSO and reverse proxy |
Two console variables are in the same position, read by the console but
not passed by the compose file: KEY_DEFAULT_DURATION (code default 90d)
and AGENTS_ALLOWED_ORIGINS (unset, which disables the connect endpoint
for the agent products). Add them to the console service's
environment: when you need them.
Two notes on values. REDIS_PASSWORD is empty because the Redis container
starts without requirepass; setting the variable alone makes the clients
send a password Redis does not expect. LANGFUSE_PUBLIC_KEY and
LANGFUSE_SECRET_KEY keep their pk-lf- and sk-lf- prefixes; bootstrap
fills only the random part.
Production values
On a public deployment the URLs above become the real ones: LIBRECHAT_URL
and CONSOLE_URL to the hostnames users open, LANGFUSE_NEXTAUTH_URL to
Langfuse's, and ALLOW_REGISTRATION=false once the admin account exists.
Pin NUFI_CONSOLE_TAG to a version.
The wrapper stack
deploy/railway/.env.example is a different, shorter file for the app on
its own:
| Variable | Default | What it does |
|---|---|---|
IMAGE_TAG | main | the app image tag |
DOMAIN_CLIENT, DOMAIN_SERVER | http://localhost:3081 | the app's public URL |
APP_TITLE, ALLOW_REGISTRATION, ALLOW_EMAIL_LOGIN | NuFi Chat, true, true | as above; set the title to NuFi |
COOKIE_DOMAIN, COOKIE_SAMESITE | empty, strict | passed through to the app here |
JWT_SECRET, JWT_REFRESH_SECRET, CREDS_KEY, CREDS_IV | generated by its bootstrap.sh | |
BACKEND_BASE_URL, BACKEND_API_KEY | the gateway (https://api.codechi.me/v1 for NuFi's own) and a key for it | |
CONSOLE_URL | empty | the Console entry |
RAG_GOOGLE_API_KEY, EMBEDDINGS_PROVIDER, EMBEDDINGS_MODEL, RAG_PORT | , google_genai, gemini-embedding-001, 8000 | the file-upload RAG service |
SHARED_DOCKER_NETWORK | commented | join the platform stack's network on the same host |
LITELLM_SYNC_ENABLED, LITELLM_BASE_URL, LITELLM_MASTER_KEY | false, , | mirror admin-created endpoints into a central gateway; off by default |
AGENTS_URL is referenced by that stack's librechat.yaml too, and not
passed by its compose file either. On Railway the service variables reach
the container directly, which is why the entry shows there.
Everything else
The console, the admin panel, NuFi Studio and NuFi Works are configured on their own pages: Work on the console lists every variable the console reads, Work on the admin panel the admin panel's, Installing NuFi Studio, Installing NuFi Works and Single sign-on for the agent apps the rest.