Skip to content

Environment Variables

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:

IconOverlay variableInherited variable
Copycopies KEY=value. A masked value is not in the page, so that row copies the key alone, and its title says sosame
Editopens the editor at that variable with its box focusedopens the editor with a prefilled override row. Nothing is pending until the value changes
Deleteopens the editor with the variable marked Will be removed. Nothing changes until Save and its confirmationdisabled, 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.

  1. 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.
  2. Write — by line surgery, never a YAML round trip.
  3. Record the previous value in the wallet.
  4. 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.

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.

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.

Terminal window
# What the overlay declares, plus what the container has inherited
curl -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 id
curl -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=.

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.

Terminal window
# LLM provider — one provider, one pinned model, no cross-model fallback (since 2026-10-07)
AI_PROVIDER=neuralwatt
NEURALWATT_API_KEY=... # secret, set through the console env editor
LLM_MODEL=deepseek-v4.1-flash # set in docker-compose.env.yml
LLM_VISION_MODEL=... # attachments / image reading
# Service config
PORT=9300
NODE_ENV=production
# Downstream services (compose service names)
BACKEND_API_URL=http://backend:8080
METERING_BACKEND_URL=http://backend:8080
ANALYZER_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:9100
OPENSEARCH_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 above
WARDEN_JUDGE_ENABLED=true # "false" skips the AI judge in the relay and (since 2026-10-21) in Cortex
CORTEX_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-21
Terminal window
AI_PROVIDER=neuralwatt
NEURALWATT_API_KEY=...
LLM_MODEL=deepseek-v4.1-flash
LLM_MAX_CONCURRENT=3 # unset = 3; this process's share of the key's 5 slots; 0 = no cap
PORT=9001 # 9000 on prod
NODE_ENV=production
OPENSEARCH_URL=https://opensearch:9200
METERING_BACKEND_URL=http://backend:8080
WARDEN_BACKEND_URL=http://backend:8080
WARDEN_INTERNAL_TOKEN=... # /internal/scan/* auth; also enables ingest-triggered collector scans
AGENT_SCAN_ENABLED=... # "true" = periodic collector-log sweep; ingest-triggered scans run regardless
AGENT_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,303
AGENT_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)”
Terminal window
PORT=9100
NODE_ENV=production
OPENSEARCH_URL=https://opensearch:9200
OPENSEARCH_ML_MODEL_ID=<model-id-from-semantic-setup>
SEMANTIC_ADMIN_TOKEN=... # operator token for model/reindex routes; required in production (2026-10-10)

Spring reads these from the container environment (relaxed binding of application.properties keys):

Terminal window
SERVER_PORT=8080 # published on the host as 9800
SPRING_DATASOURCE_URL=jdbc:postgresql://<postgres-service>:5432/leapfrog
SPRING_DATASOURCE_USERNAME=...
SPRING_DATASOURCE_PASSWORD=...
OPENSEARCH_URL=https://opensearch:9200
LOGWRITE_ANALYZEAI_URL=http://analyzer:9001 # analyzer:9000 on prod
OPENSEARCH_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 403
LOGRITE_OPENSEARCH_ORPHAN_JANITOR_ENABLED=false # daily purge of OpenSearch data whose workspace no longer exists
LOGRITE_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 encryption
WARDEN_KMS_MASTER_KEY=... # current key: wraps every new DEK
WARDEN_KMS_MASTER_KEY_PREVIOUS= # only during a rotation; see AI Warden → rotating the master key

JWT lifetime stays security.jwt.expiration-time=14400000 (4 hours; the sliding X-Refresh-Token response header keeps active users signed in).

Terminal window
PORT=9500
OPENSEARCH_URL=https://opensearch:9200
# defaults in config.py — the compose service names; override only for a local run
LOGRITE_BACKEND_URL=http://backend:8080
LOGRITE_AI_URL=http://ai-service:9300
LOGRITE_SEMANTIC_URL=http://semantic-search:9100
LOGRITE_INGEST_URL=http://ingest-shim:9700
{
"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”
Terminal window
PORT=9400
BACKEND_URL=http://localhost:8080
AI_SERVICE_URL=http://localhost:9300
POLL_INTERVAL_MS=60000
AGENT_TIMEOUT_MS=120000

opensearchproject/opensearch:3.3.1, configured through container environment, not a host YAML file:

Terminal window
discovery.type=single-node
cluster.name=logrite-opensearch-dev
OPENSEARCH_JAVA_OPTS=-Xms4g -Xmx4g # dev heap, from docker-compose.env.yml
path.repo=/usr/share/opensearch/snapshots

Both clusters are single-node with cluster.default_number_of_replicas: 0 (persistent cluster setting), so they report green.