/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.
Umami
The patched Umami invendor/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.
Privacy switches
Cuts the IP to /24 (IPv4) or /48 (IPv6) before the geo lookup. Region and city are never stored.
compose.yaml: "1"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"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"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.Layer 1, real browser. Requires an
Origin that matches one of the site’s domains and the Fetch Metadata headers.compose.yaml: "1"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"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.txtLayer 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 networkUpstream 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 inapps/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 byapps/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 inapps/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
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).