Skip to main content
Lumenstat is configured with environment variables. On a server, secrets live in /srv/lumenstat/.env, which bootstrap.sh creates, and everything else is written into deploy/compose.yaml. This page lists every variable by service, with the default in the code and the value deploy/compose.yaml sets. Values on this page are placeholders, never real secrets.

The env file

bootstrap.sh writes /srv/lumenstat/.env on the first run, with mode 600, and never overwrites it. Compose reads it with --env-file and passes each value to the services that need it.
/srv/lumenstat/.env
Compose refuses to start when POSTGRES_PASSWORD, UMAMI_APP_SECRET, SCF_COLLECTOR_SECRET, WRITER_OPERATOR_KEY or UMAMI_ADMIN_PASSWORD is missing.
Two of these cannot be changed by editing the file alone.
  • POSTGRES_PASSWORD: PostgreSQL sets the password only when it creates the pgdata volume. Change it in the database first (ALTER USER lumenstat PASSWORD …), then in the file.
  • UMAMI_ADMIN_PASSWORD: bootstrap.sh replaces only the default password umami. Change the admin password in Umami first, then in the file, or enrollment stops working because the dashboard can no longer sign in as admin.

Umami

The patched Umami in vendor/umami. The switches are read in src/lib/scf, added by the patches in patches/umami. Each one is off unless set, so the fork behaves like upstream Umami without them.
The on/off switches are on when set to any non-empty value, including 0 and false. To turn one off, remove the variable.

Privacy switches

flag
default:"off"
Cuts the IP to /24 (IPv4) or /48 (IPv6) before the geo lookup. Region and city are never stored.compose.yaml: "1"
flag
default:"off"
Ignores caller-supplied ip, userAgent, browser, os and device in the collect payload. These come from the request itself. screen is still read.compose.yaml: "1"
flag
default:"off"
Stores utm_* parameters only. Drops every other query parameter, ad click ids (gclid, fbclid, msclkid, ttclid, li_fat_id, twclid) and the referrer’s query string.compose.yaml: "1"
flag
default:"off"
Replaces Umami’s date-derived session salt with a random salt per UTC day, stored in Redis under scf:salt:<date> with a 48-hour TTL and never in the database. The first instance to write it wins. Without REDIS_URL, each process keeps its own salt in memory, which is correct only for a single instance.compose.yaml: "1"

Quality checks

Each layer of the bot and inflation defense has its own switch. Events that fail are stored as flagged with a reason and not counted.
flag
default:"off"
Layer 1, real browser. Requires an Origin that matches one of the site’s domains and the Fetch Metadata headers.compose.yaml: "1"
integer
default:"0 (off)"
Layer 2, proof of JavaScript. The number of leading zero bits the tracker’s x-lumenstat-pow proof must have, from 1 to 24. Any other value turns the check off. The tracker always works to 10 bits, so a value above 10 flags most real visitors.compose.yaml: "10"
path
default:"unset (off)"
Layer 3, data center. Path to a file with one CIDR range per line, # for comments. Read once per path and kept in memory: restart Umami after changing the file. If the file cannot be read, Umami logs datacenter_ranges_unreadable and the check flags nothing.compose.yaml: /etc/lumenstat/datacenter-ranges.txt, mounted read-only from deploy/datacenter-ranges.txt
integer
default:"0 (off)"
Layer 4, many visitors per network. Distinct visitors allowed per site, per /24 or /48 network, per UTC hour. Must be a positive integer. Needs REDIS_URL; without Redis the check is off.compose.yaml: "150", generous because mobile carriers put many real people behind one IPv4 network

Upstream Umami variables

deploy/compose.yaml sets these Umami variables too. Umami reads many more; see its documentation. These upstream variables interact with the patches. deploy/compose.yaml sets none of them:

Dashboard

The Next.js app in apps/dashboard. For local runs, copy apps/dashboard/.env.example to .env.local. Its values match the code defaults below. The snippet’s integrity hash is not configured. The dashboard fetches <UMAMI_URL>/v/<TRACKER_VERSION>/l.js, hashes it with SHA-384 and caches the result for an hour. If the fetch fails, the snippet is shown without integrity. The image sets NODE_ENV=production, which marks the session cookie Secure, and PORT=3200.
PUBLIC_UMAMI_URL and PUBLIC_COLLECTOR_URL are fixed to the production hostnames in deploy/compose.yaml. Edit them for your domain before deploying. On staging, snippets show the production hostnames until this is made configurable.

Collector

The OpenTelemetry Collector, configured by apps/collector/config.yaml. The local Compose file defaults SCF_WRITER_ENDPOINT to http://writer:8080. In host mode, set it to http://host.docker.internal:8090. Fixed in config.yaml, not in the environment: OTLP on 4317 (gRPC) and 4318 (HTTP), health check on 13133, histogram buckets of 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500 and 10000 ms, a spanmetrics flush every 15 s, batches of up to 2,048 or 5 s, and retries to the writer for up to 10 minutes with a queue of 5,000.

Writer

The Node service in apps/writer. It applies its migrations on start. The writer’s command-line tools read DATABASE_URL as well. On a server, run them inside the container, with the dc alias from the overview:

PostgreSQL, Redis and cloudflared

Redis writes nothing to disk and never evicts keys early: a lost salt would split visitor counts mid-day.

Local development

scripts/setup-env.sh writes the repository’s .env with random secrets, unless it exists. .env.example shows the same names with placeholder values:
.env
Locally the operator key is set directly as SCF_BOOTSTRAP_OPERATOR_KEY. On a server it is WRITER_OPERATOR_KEY, mapped to the same writer variable. The example API in examples/test-api reads SCF_OTLP_ENDPOINT (default http://localhost:4318), SCF_INGEST_KEY, OTEL_SERVICE_NAME (default scf-test-api) and OWN_APM_CONSOLE (1 also prints spans to the console, standing in for a project’s own APM).
Last modified on November 14, 2026