Environment Variables
Changing a variable on a running estate
Section titled “Changing a variable on a running estate”Use management.logrite.ai → the service → Environment → Edit environment.
This is the supported path; hand-editing the file over ssh is what took the
backend down on 2026-09-29, when it grew a second backend: mapping key and
compose refused the whole stack with half a credential rotation applied.
What the editor edits, and what it deliberately does not
Section titled “What the editor edits, and what it deliberately does not”The Environment tab shows docker inspect — the effective environment,
merging the image’s own ENV, the generated compose file and the operator
overlay. The editor changes only /opt/logrite-stack/docker-compose.env.yml,
the overlay, and shows everything else on a read-only “Inherited” tab.
Edit, Delete and Copy on every row of the Environment tab
Section titled “Edit, Delete and Copy on every row of the Environment tab”Since 2026-10-01 each variable on the Environment tab carries three icons. The
first two appear only when the console runs with --allow-actions, as does
Edit environment:
| Icon | Overlay variable | Inherited variable |
|---|---|---|
| Copy | copies KEY=value. A masked value is not in the page, so that row copies the key alone, and its title says so | same |
| Edit | opens the editor at that variable with its box focused | opens the editor with a prefilled override row. Nothing is pending until the value changes |
| Delete | opens the editor with the variable marked Will be removed. Nothing changes until Save and its confirmation | disabled, with the reason in its title. The overlay never held it, so removing it there would change nothing in the container. Override it with Edit instead |
Which keys are the overlay’s is read once per open service from
GET /api/env.
The order, which is the safety property
Section titled “The order, which is the safety property”- Validate — the whole stack is checked with the change staged at a temp
path (
docker compose config -q) and with a loader that refuses duplicate keys. PyYAML’s default keeps the last duplicate and reports success, so a naive parse cannot see the failure that caused the outage. - Write — by line surgery, never a YAML round trip.
- Record the previous value in the wallet.
- Recreate the container, if “Recreate the container now” is ticked.
A refusal at step 1 costs nothing. The same mistake made after step 2 takes the service down and only then reports itself.
$ in a value is data
Section titled “$ in a value is data”Compose interpolates ${VAR} in the YAML before docker sees it, so a
password containing $ would reach the container as something neither side
chose — the same defect that baked localhost:8080 into a health probe and
produced 53 consecutive failures against a service that was serving fine.
Values are stored $$-escaped and shown unescaped, so what you type is what
the container gets.
Secrets are write-only
Section titled “Secrets are write-only”A variable whose name matches PASSWORD|SECRET|TOKEN|KEY|CRED|PEM|PRIVATE|DSN|AUTH|WEBHOOK|PASS
shows its length and a 12-character digest, never its value. Leaving its box
blank keeps it unchanged. That is the same digest form every rotation in this
estate is verified with — and a value rendered in a browser is a value in a
screenshot.
The wallet: going back one, two or three values
Section titled “The wallet: going back one, two or three values”Every change records what the variable held before it, with the time, the operator and a digest. Restore is by entry id: the server reads the old value off the host and writes it straight into the overlay, so a past password never reaches the browser, the network or the clipboard even while being restored.
Retention is the 10 newest values per variable — per key, not per file, so one busy variable cannot prune a quiet one’s only history.
The wallet lives at /var/lib/logrite-deploy/env-history.json, mode 0600 —
tighter than the overlay’s 0640, which has to stay group-readable because
compose reads it too.
# What the overlay declares, plus what the container has inheritedcurl -s "http://127.0.0.1:8090/api/env?host=dev&service=backend"
# Previous values — ids, times, digests and lengths. Never values.curl -s "http://127.0.0.1:8090/api/env/history?host=dev&service=backend"
# Change one. apply:false writes the overlay without recreating the container.curl -s -X POST http://127.0.0.1:8090/api/env \ -H 'Content-Type: application/json' \ -d '{"host":"dev","service":"backend","changes":{"LOG_LEVEL":"debug"},"apply":true}'
# Put a previous value back, by wallet idcurl -s -X POST http://127.0.0.1:8090/api/env/restore \ -H 'Content-Type: application/json' \ -d '{"host":"dev","service":"backend","id":"<id from /api/env/history>"}'POST /api/env and /api/env/restore return 202 with a job id — the work
is queued and its log is streamed, like a deploy. Poll /api/deployment?id= and
/api/deployment/log?id=.
AI Service (ai-service compose service)
Section titled “AI Service (ai-service compose service)”Values as set in /opt/logrite-stack/docker-compose.yml and the docker-compose.env.yml overlay
on dev. Compose service names resolve on the docker network; there is no localhost hop.
# LLM provider — one provider, one pinned model, no cross-model fallback (since 2026-10-07)AI_PROVIDER=neuralwattNEURALWATT_API_KEY=... # secret, set through the console env editorLLM_MODEL=deepseek-v4.1-flash # set in docker-compose.env.ymlLLM_VISION_MODEL=... # attachments / image reading
# Service configPORT=9300NODE_ENV=production
# Downstream services (compose service names)BACKEND_API_URL=http://backend:8080METERING_BACKEND_URL=http://backend:8080ANALYZER_API_URL=http://analyzer:9001 # analyzer:9000 on prod. The ingest-shim needs it too: it sends the ingest-triggered collector scan. No default since 2026-10-21: unset → "[shim] collector scan trigger disabled: ANALYZER_API_URL unset"SEMANTIC_API_URL=http://semantic-search:9100OPENSEARCH_URL=https://opensearch:9200
# Cortex workspace rules and the AI Warden relay (warden, ingest-shim share the image)WARDEN_INTERNAL_TOKEN=... # required for Cortex workspace rules; unset → rules off, startup log "[workspace-rules] disabled: …", /health workspaceRules:"unconfigured"WARDEN_BACKEND_URL=http://backend:8080 # required for Cortex workspace rules, as aboveWARDEN_JUDGE_ENABLED=true # "false" skips the AI judge in the relay and (since 2026-10-21) in CortexCORTEX_RULES_MAX_STALE_MS=900000 # Cortex workspace rules: keep the last good bundle this long while the backend is unreachable (15 min)WARDEN_BINDING_MAX_STALE_MS=900000 # relay: serve the last good relay-key binding this long while the backend resolve fails (15 min; since 2026-10-21)ENFORCE_TENANT_FROM_JWT=true # .env.example default since 2026-10-21Analyzer (analyzer compose service)
Section titled “Analyzer (analyzer compose service)”AI_PROVIDER=neuralwattNEURALWATT_API_KEY=...LLM_MODEL=deepseek-v4.1-flashLLM_MAX_CONCURRENT=3 # unset = 3; this process's share of the key's 5 slots; 0 = no capPORT=9001 # 9000 on prodNODE_ENV=productionOPENSEARCH_URL=https://opensearch:9200METERING_BACKEND_URL=http://backend:8080WARDEN_BACKEND_URL=http://backend:8080WARDEN_INTERNAL_TOKEN=... # /internal/scan/* auth; also enables ingest-triggered collector scansAGENT_SCAN_ENABLED=... # "true" = periodic collector-log sweep; ingest-triggered scans run regardlessAGENT_SCAN_JUDGE_MAX_LINES=0 # both estates: collector scans use framework + pattern rules only, no judge (default 80)AGENT_SCAN_SKIP_DEMO_WORKSPACES=... # seeded demo lines (demo:true) in these workspaces are not scanned: dev 3103, prod 302,303AGENT_SCAN_TRIGGER_DELAY_MS=6000 # debounce before a triggered scan reads its window (2026-10-21)AGENT_SCAN_TRIGGER_REREAD_MS=5000 # one re-read after this delay when the first read missed the triggering batch (2026-10-21)Semantic Search (semantic-search compose service)
Section titled “Semantic Search (semantic-search compose service)”PORT=9100NODE_ENV=productionOPENSEARCH_URL=https://opensearch:9200OPENSEARCH_ML_MODEL_ID=<model-id-from-semantic-setup>SEMANTIC_ADMIN_TOKEN=... # operator token for model/reindex routes; required in production (2026-10-10)Backend (backend compose service)
Section titled “Backend (backend compose service)”Spring reads these from the container environment (relaxed binding of application.properties
keys):
SERVER_PORT=8080 # published on the host as 9800SPRING_DATASOURCE_URL=jdbc:postgresql://<postgres-service>:5432/leapfrogSPRING_DATASOURCE_USERNAME=...SPRING_DATASOURCE_PASSWORD=...OPENSEARCH_URL=https://opensearch:9200LOGWRITE_ANALYZEAI_URL=http://analyzer:9001 # analyzer:9000 on prodOPENSEARCH_DASHBOARDS_HOST=http://opensearch-dashboards:5601
# Operator / data-lifecycle (2026-10-16)LOGRITE_ADMIN_TOKEN=... # X-Logrite-Admin-Token for /internal/admin/*; unset = those routes answer 403LOGRITE_OPENSEARCH_ORPHAN_JANITOR_ENABLED=false # daily purge of OpenSearch data whose workspace no longer existsLOGRITE_OPENSEARCH_ORPHAN_JANITOR_CRON=0 40 4 * * *LOGRITE_POLICY_DROP_LEGACY_TABLES=true # set on dev and prod 2026-10-16; drops the pre-policy rule tables once verified
# Warden envelope encryptionWARDEN_KMS_MASTER_KEY=... # current key: wraps every new DEKWARDEN_KMS_MASTER_KEY_PREVIOUS= # only during a rotation; see AI Warden → rotating the master keyJWT lifetime stays security.jwt.expiration-time=14400000 (4 hours; the sliding
X-Refresh-Token response header keeps active users signed in).
MCP Server (mcp-server compose service)
Section titled “MCP Server (mcp-server compose service)”PORT=9500OPENSEARCH_URL=https://opensearch:9200# defaults in config.py — the compose service names; override only for a local runLOGRITE_BACKEND_URL=http://backend:8080LOGRITE_AI_URL=http://ai-service:9300LOGRITE_SEMANTIC_URL=http://semantic-search:9100LOGRITE_INGEST_URL=http://ingest-shim:9700Webhook Server (config/webhook.json)
Section titled “Webhook Server (config/webhook.json)”{ "port": 9999, "secret": "<github-webhook-hmac-secret>", "repos": [...]}Discord Notifications (config/webhook.json)
Section titled “Discord Notifications (config/webhook.json)”{ "discord": { "webhookUrl": "https://discord.com/api/webhooks/..." }}Agent Runner (logrite-agent-runner/.env) — Proposed
Section titled “Agent Runner (logrite-agent-runner/.env) — Proposed”PORT=9400BACKEND_URL=http://localhost:8080AI_SERVICE_URL=http://localhost:9300POLL_INTERVAL_MS=60000AGENT_TIMEOUT_MS=120000OpenSearch (opensearch compose service)
Section titled “OpenSearch (opensearch compose service)”opensearchproject/opensearch:3.3.1, configured through container environment, not a host YAML
file:
discovery.type=single-nodecluster.name=logrite-opensearch-devOPENSEARCH_JAVA_OPTS=-Xms4g -Xmx4g # dev heap, from docker-compose.env.ymlpath.repo=/usr/share/opensearch/snapshotsBoth clusters are single-node with cluster.default_number_of_replicas: 0 (persistent cluster
setting), so they report green.