Configuration¶
Every environment variable LogsTotal reads from .env, how to generate its secrets, and how the Docker Compose profiles work.
All application configuration lives in .env in the install directory.
.env.example is the
copy-paste template with safe defaults; this page is the annotated reference. The fleet
deploy tooling reads its own DEPLOY_* variables from deploy.env instead — see
DEPLOY_* reference. After editing .env, follow
Apply configuration changes.
Generating secrets and keys¶
Generate everything at once, on the deployment host, from the install directory:
./logstotal gen-secrets # print a copy-paste block of all secrets
./logstotal gen-secrets -- --write # fill them into .env (only empty/placeholder keys; never clobbers real values)
This covers SECRET_KEY, ADMIN_PASSWORD (printed once when newly generated),
POSTGRES_PASSWORD, REDIS_PASSWORD, the Garage-style S3_ACCESS_KEY/S3_SECRET_KEY,
and GARAGE_RPC_SECRET/GARAGE_ADMIN_TOKEN. --write only touches keys that are still
unset or a placeholder, so it is safe to re-run. To choose your own admin password, set
ADMIN_PASSWORD in .env before running it.
To generate a single value instead, run one of these anywhere python3 or Docker is available:
| Key | Format | Command |
|---|---|---|
SECRET_KEY |
64 hex chars — signs JWTs and sessions | python3 -c "import secrets; print(secrets.token_hex(32))" |
POSTGRES_PASSWORD, REDIS_PASSWORD, S3_SECRET_KEY, GARAGE_RPC_SECRET |
64 hex chars | python3 -c "import secrets; print(secrets.token_hex(32))" |
S3_ACCESS_KEY |
GK + 24 hex chars |
python3 -c "import secrets; print(f'GK{secrets.token_hex(12)}')" |
GARAGE_ADMIN_TOKEN |
opaque bearer token, not hex | python3 -c "import secrets; print(secrets.token_urlsafe(24))" |
BASIC_AUTH_HASH |
bcrypt, with COMPOSE_PROFILES=proxy |
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'yourpassword' — or let ./logstotal proxy:enable / ./logstotal deploy do it (they pass the password over stdin, keeping it out of ps) |
Paste the basic-auth hash into BASIC_AUTH_HASH with single quotes, so Docker Compose does not treat $ as variable expansion.
Concurrency: HUEY_WORKERS, TOOL_MAX_WORKERS and the per-tool threads multiply into CPU pressure — see Scaling for the model and ./logstotal recommend-scaling.
Docker Compose profiles¶
COMPOSE_PROFILES in .env selects which bundled services Docker Compose starts alongside web, worker and Redis. Combine them with commas:
| Profile | Adds | Used by |
|---|---|---|
postgres |
Bundled PostgreSQL (Docker volume postgres_data) |
With PostgreSQL, fleets |
s3 |
Bundled Garage S3-compatible object storage | Fleets |
proxy |
Caddy with automatic TLS (+ optional basic auth) | HTTPS with Caddy |
workers |
socat relays that publish Redis, PostgreSQL and Garage on the REDIS_EXPOSE, POSTGRES_EXPOSE and GARAGE_EXPOSE addresses, for remote workers. Each defaults to loopback (127.0.0.1) |
Fleet installation |
Examples:
COMPOSE_PROFILES=postgres # single host, PostgreSQL instead of SQLite
COMPOSE_PROFILES=proxy # single host, HTTPS via Caddy
COMPOSE_PROFILES=postgres,s3,workers # fleet control plane (bundled services)
COMPOSE_PROFILES=postgres,s3,proxy,workers # fleet control plane behind HTTPS
COMPOSE_PROFILES decides whether the bundled PostgreSQL is used. The image entrypoint switches from SQLite to PostgreSQL only when POSTGRES_PASSWORD is set and postgres is in COMPOSE_PROFILES (or an external POSTGRES_HOST is configured), then builds the database URLs itself — no manual URL editing. The rule cuts both ways:
- A
POSTGRES_PASSWORDwithout thepostgresprofile keeps the app on SQLite and logs a warning, rather than waiting for apostgreshost Compose never started. COMPOSE_PROFILES=postgreswithout a password also stays on SQLite.
An explicit DATABASE_URL always wins. ./logstotal backup resolves the database the same way, so a leftover password never makes it dump the wrong engine. Avoid # @ % ? in the password, or URL-encode them.
Cookies over plain HTTP¶
With DEBUG=false, auth cookies are marked Secure, and browsers silently drop them on http:// — login appears to succeed but never sticks. Set COOKIE_INSECURE=true for any deployment served over plain HTTP (Docker without a proxy, local development on :8000). Once TLS is in front — the proxy profile or your own reverse proxy — set COOKIE_INSECURE=false (or remove it); HTTPS with Caddy says which ENABLE_HSTS value goes with each TLS mode. ./logstotal quickstart sets COOKIE_INSECURE=true for a plain-HTTP start, and ./logstotal proxy:enable sets it to match the TLS mode it configures.
The browser's clipboard API also requires HTTPS, so every "Copy" button falls back to an older mechanism on plain HTTP. It works, and says so when a strict browser refuses it.
AI provider base URLs¶
AI providers are configured on /admin/ai, not in .env — the base URL, model, API token, timeout and system prompt all live there. Two things about the base URL look like bugs the first time you meet them.
A private or internal address is allowed by default. The intended setup for this feature is a model running on the same machine, so http://localhost:11434/v1 (Ollama) or http://127.0.0.1:1234/v1 (LM Studio) is expected. AI_REQUIRE_PUBLIC_HOST=true restricts base URLs to publicly-routable addresses, which is the right setting when your admin accounts are not fully trusted — and it breaks every local model, so it is not the default. Cloud metadata addresses are refused either way. See AI job analysis for the full boundary.
In Docker, localhost is the container, not your machine. The completion request is made by the worker, so the base URL is resolved inside the worker container. If Ollama runs on the Docker host, http://localhost:11434/v1 reaches the worker container itself, and the run fails with "could not reach the provider". Use the host gateway instead:
http://host.docker.internal:11434/v1
That name resolves on Docker Desktop (macOS, Windows) and OrbStack. On Linux, host.docker.internal needs an explicit mapping — add extra_hosts: ["host.docker.internal:host-gateway"] to the worker service, or point the base URL at the host's LAN or WireGuard address. Also make sure the model listens on more than loopback: Ollama binds 127.0.0.1 by default and needs OLLAMA_HOST=0.0.0.0 before anything outside the host can reach it.
The Test button on /admin/ai is the fastest way to tell these failures apart: it runs a real one-word completion and reports the error immediately. Test runs in the web process; real runs happen in the worker. On a single host both reach the same network, so a passing test means the feature works. In a fleet, a passing test only proves the control plane can reach the model — the base URL must also resolve and connect from every worker host.
Environment variable reference¶
Every setting, grouped by concern. A default applies when the key is absent from .env.
Required¶
| Variable | Description |
|---|---|
SECRET_KEY |
JWT signing key (must change from the default — the app refuses to start with it) |
ADMIN_EMAIL / ADMIN_PASSWORD |
The first admin account, created on a start that finds no administrator (the web container checks on every start). Once one exists, changing either creates or changes nothing — see Account recovery. Refused: changeme123 and other common defaults, passwords under 8 characters, and passwords containing the email |
DATABASE_URL |
Async database URL (default: SQLite; postgresql+asyncpg://... for PostgreSQL) |
SYNC_DATABASE_URL |
Sync database URL for workers and migrations — derived from DATABASE_URL when unset |
AUTO_MIGRATE |
Run database migrations automatically when the web process starts (default: true; set false only while investigating a failed migration — see Database migrations) |
Redis and Huey¶
| Variable | Description |
|---|---|
REDIS_HOST / REDIS_PORT |
Huey broker connection (default: localhost:6379) |
REDIS_PASSWORD |
Redis AUTH password; leave empty for no auth |
REDIS_URL |
Full Redis connection URL — overrides host, port and password (for remote Redis or TLS) |
REDIS_EXPOSE |
Address the workers profile publishes Redis on for remote workers (default 127.0.0.1:6379; e.g. 10.0.0.1:6379). A non-loopback address requires REDIS_PASSWORD or the app refuses to start |
HUEY_WORKERS |
Worker threads per worker process; each thread runs one job at a time. Default 2 in docker-compose.yml (and for ./logstotal worker), 4 in docker-compose.worker.yml on dedicated worker hosts |
PostgreSQL¶
Used with COMPOSE_PROFILES=postgres or an external PostgreSQL server.
| Variable | Description |
|---|---|
POSTGRES_PASSWORD |
Required for PostgreSQL (see Docker Compose profiles for when it takes effect) |
POSTGRES_USER / POSTGRES_DB |
Optional, both default to logstotal |
POSTGRES_HOST / POSTGRES_PORT |
Optional, default to postgres / 5432 (the Compose service) |
POSTGRES_EXPOSE |
Address the workers profile publishes PostgreSQL on for remote workers (default 127.0.0.1:5432; e.g. 10.0.0.1:5432) |
DB_POOL_SIZE / DB_MAX_OVERFLOW / DB_POOL_RECYCLE |
Connection pool tuning (defaults: 5 / 10 / 300; ignored for SQLite) |
S3 storage¶
Used with STORAGE_BACKEND=s3.
| Variable | Description |
|---|---|
STORAGE_BACKEND |
local (default) or s3 — required for a fleet, where workers do not share a filesystem |
S3_ENDPOINT / S3_BUCKET |
S3-compatible endpoint URL and bucket name (bucket default: logstotal). With the s3 profile and no S3_ENDPOINT, the bundled Garage (http://garage:3900) is used |
S3_ACCESS_KEY / S3_SECRET_KEY |
S3 credentials (Garage: GK-prefixed key + 64-char hex secret). The bundled Garage creates this key and bucket on first start |
S3_REGION |
S3 region (default: logstotal) |
GARAGE_RPC_SECRET |
Secret for the bundled Garage node's internal RPC (s3 profile), 64 hex chars. Unset, Garage starts with a built-in default and logs a warning — set it (./logstotal gen-secrets does) |
GARAGE_ADMIN_TOKEN |
Token for the bundled Garage's admin API (s3 profile). Unset, Garage uses a built-in default and logs a warning — set it (./logstotal gen-secrets does) |
GARAGE_EXPOSE |
Address the workers profile publishes Garage's S3 API on for remote workers (default 127.0.0.1:3900) |
Reverse proxy and TLS¶
Used with COMPOSE_PROFILES=proxy. The four PROXY_TLS modes, what each needs, and the COOKIE_INSECURE / ENABLE_HSTS pair each one requires are in HTTPS with Caddy. ./logstotal proxy:enable -- --tls <mode> — or DOMAIN=... QUICKSTART_TLS=<mode> ./logstotal quickstart on a first deploy — sets all of them together. They have to agree: when they do not, login fails silently.
| Variable | Description |
|---|---|
DOMAIN |
The name Caddy serves. Letters, digits, . and - only — an IPv4 literal works, an IPv6 literal does not (see Known limitations) |
PROXY_TLS |
How TLS is terminated: acme (default), internal, custom or off |
ACME_EMAIL |
Contact address for the certificate authority, so it can warn about expiry. PROXY_TLS=acme only |
PROXY_TLS_CERT / PROXY_TLS_KEY |
In-container paths under /etc/caddy/certs (bind-mounted from ./certs). PROXY_TLS=custom only; both are required together |
WEB_PORT |
(also applies without the proxy — see below) The web service's port mapping, not a port number |
BASIC_AUTH_USER / BASIC_AUTH_HASH |
Optional Caddy basic auth. Set both or neither. Generate the hash with the recipe in Generating secrets and keys and paste it with single quotes |
Important
WEB_PORT is a Docker Compose port mapping in HOST:CONTAINER form — not a port number. WEB_PORT=9000 publishes container port 9000, which nothing listens on, so the site becomes unreachable without any error.
- Change the host port:
WEB_PORT=9000:8000 - Bind to loopback only (what the proxy profile sets):
WEB_PORT=127.0.0.1:8000:8000 - Default:
WEB_PORT=8000:8000
The container always listens on 8000; only the left-hand side is yours to choose.
Docker Compose¶
| Variable | Description |
|---|---|
COMPOSE_PROFILES |
Which bundled services start: postgres, s3 (Garage), proxy (Caddy), workers (publish Redis, PostgreSQL and Garage to remote workers) — see Docker Compose profiles |
LOGSTOTAL_IMAGE_TAG |
Tag of the logstotal and logstotal-garage images Compose builds and runs (default: latest). A bundle install sets it for you; you rarely need it otherwise |
DOCKER_HOST_WORKDIR |
Host path of the install directory, so the worker can mount job files into tool containers. Set by Compose to the directory you run it from — start the stack from the install directory |
Application¶
| Variable | Description |
|---|---|
DEBUG |
Debug mode (default: false). Never on an internet-facing host — see Security escape hatches |
COOKIE_INSECURE |
When true, drops the Secure flag on auth cookies (plain-HTTP deployments only — see Cookies over plain HTTP) |
DISABLE_CSP |
Drop the Content-Security-Policy header — for local testing over an IP. With DEBUG=false the app refuses to start unless I_ACCEPT_DISABLE_CSP_IN_PROD=true is also set |
LOG_LEVEL |
Log verbosity for the web process, uvicorn and the Huey worker: DEBUG, INFO, WARNING, ERROR (default: INFO) |
LOG_FORMAT |
text (human) or json (one object per line, for a log shipper). Both processes honour it (default: text) |
REQUEST_LOG_ENABLED |
Log one line per request with status and duration (default: false). /static/ and /health are never logged; the request id is set either way |
ACTIVITY_RETENTION_DAYS |
How long activity-log rows are kept, in days; 0 keeps forever (default: 90). Capture itself is the activity_log_enabled site setting, not an environment variable |
BACKGROUND_TASK_RETENTION_DAYS |
How long finished background-task rows are kept, in days; 0 keeps forever (default: 30). Only finished rows are pruned |
UPLOAD_RETENTION_DAYS |
Age at which an uploaded log file and its jobs are deleted, in days (default: 0 = never). The age counts from the newer of the upload and the most recent job on that file, so re-uploading identical content as a new job or resubmitting it keeps the file. Off by default because this removes submitted evidence, not derived data. /admin/storage shows the effective value; the sweep schedule is in Storage and retention |
UPLOAD_DIR |
Local upload directory (default: uploads) |
MAX_UPLOAD_SIZE_MB |
Maximum upload file size in MB (default: 500) |
UPLOAD_RATE_LIMIT_PER_MINUTE |
Anonymous uploads per client IP per minute (default: 30; 0 = unlimited) |
AUTHENTICATED_UPLOAD_RATE_LIMIT_PER_MINUTE |
Authenticated uploads per account per minute, shared by its cookies and tokens (default: 60; 0 = unlimited) |
PREVIEW_RATE_LIMIT_PER_MINUTE |
File-type previews per client IP per minute (default: 120; 0 = unlimited) |
UPLOAD_MAX_CONCURRENT |
Upload requests admitted at once across all web processes sharing Redis (default: 4; effective minimum: 1). See Ingestion API |
LOGIN_RATE_LIMIT_PER_MINUTE |
POST /auth/cookie/login requests per client IP per minute (default: 20; 0 = unlimited) |
RESUBMIT_RATE_LIMIT_PER_MINUTE |
POST /jobs/resubmit requests per client IP per minute (default: 10; 0 = unlimited) |
ENABLE_HSTS |
Send the Strict-Transport-Security header (default: false; see HTTPS with Caddy for which TLS modes want it) |
HSTS_MAX_AGE |
HSTS max-age in seconds (default: 63072000, two years) |
TRUST_PROXY_HEADERS |
Trust forwarded client-IP headers when the request comes from a trusted proxy (default: false) |
TRUSTED_PROXY_CIDRS |
Comma-separated CIDRs allowed to supply forwarded headers (default: 127.0.0.1/32,::1/128; * only behind fully trusted ingress). List every hop — the X-Forwarded-For chain is read from the right, stopping at the first address not listed here. See Production hardening checklist |
ENRICHMENT_ENCRYPTION_KEY |
Separate key for encrypting stored secrets at rest — enrichment-service and AI-provider API tokens, rule webhook signing secrets (default: derived from SECRET_KEY; rotating either makes every stored secret unreadable) |
API_TOKEN_RATE_LIMIT_PER_MINUTE |
Requests per API token per minute under /api/v1/*, /intel/ioc-feed, /taxii2/* and /intel/cases/* (default: 120). Requests without a token are counted per client IP, except under /intel/cases/*, which is limited only for token requests because the same prefix serves the browser's case pages |
TAXII_ENABLED |
Enable the read-only TAXII 2.1 server at /taxii2/ (default: false) |
ENRICHMENT_RATE_LIMIT_PER_MINUTE |
Outbound live-enrichment calls per service per minute (default: 30; 0 = unlimited; the cached result is served when limited) |
ENRICHMENT_REQUIRE_PUBLIC_HOST |
Restrict live-enrichment endpoints to publicly-routable addresses (default: true). The opposite default to the webhook and AI settings below, deliberately: a threat-intel API lives on the public internet, so a private address there is a mistake or an SSRF attempt. Set false to reach a self-hosted MISP or OpenCTI. Cloud metadata addresses are blocked either way — see Live enrichment |
WEBHOOK_REQUIRE_PUBLIC_HOST |
Restrict rule webhook targets to publicly-routable addresses (default: false, so internal receivers work). Set true where members are not fully trusted — see Rule webhooks |
WEBHOOK_TIMEOUT_SECONDS |
Total HTTP deadline for one webhook delivery, in seconds (default: 5); DNS validation happens before it starts |
WEBHOOK_RATE_LIMIT_PER_MINUTE |
Webhook deliveries per rule per minute (default: 10; 0 = unlimited; deliveries over the limit are recorded and dropped, never retried) |
WEBHOOK_MAX_RETRIES |
Retries after a network error, timeout or 5xx, with 60s/120s/240s backoff. A 4xx other than 429 is final (default: 3) |
WEBHOOK_DELIVERY_RETENTION_DAYS |
Days to keep the webhook delivery log (default: 30; 0 = keep forever) |
WATCH_RULES_MAX_PER_USER |
Maximum rules one user may own (default: 50) |
RULE_LIST_REQUIRE_PUBLIC_HOST |
Restrict a rule list's source URL to publicly-routable addresses (default: true) — a feed is fetched on a schedule with nobody watching. Set false for an internal mirror of a public list. The cloud metadata endpoint is refused either way, and redirects are not followed |
RULE_LIST_FETCH_TIMEOUT_SECONDS |
Per-request timeout when fetching a list's source URL (default: 15) |
RULE_LIST_FETCH_MAX_BYTES |
Largest feed that will be read (default: 1048576). A list holds at most 2,000 values of 200 characters, so a real feed is far below this |
AI_REQUIRE_PUBLIC_HOST |
Restrict AI provider base URLs to publicly-routable addresses (default: false, so a local model works). Set true where an admin account is not fully trusted — see AI provider base URLs and AI job analysis |
AI_RATE_LIMIT_PER_MINUTE |
Analysis runs one user may start per minute (default: 10; 0 = unlimited). Lower than the other limits because one click spends money or occupies a GPU |
AI_MAX_PROMPT_CHARS |
Default size of the evidence brief for job and case AI analysis, in characters (default: 60000). A provider can override the job and case limits on /admin/ai; blank fields inherit this value. System prompts are separate. An oversized brief omits evidence and says that the model's view is partial |
Note
Every *_RATE_LIMIT_PER_MINUTE is counted in Redis, so the value is the limit for the whole deployment — running more web processes does not multiply it. See Rate limit semantics.
Workers¶
| Variable | Description |
|---|---|
REGISTRY_PREFIX |
Pull images from this registry instead of Docker Hub — for a network with no route to Docker Hub that runs its own mirror. Applied to the Compose services and to the tool images named in workflows/*.yml; digests are kept, and an image that already names a registry is left alone. Empty by default. For a network with no registry, use ./logstotal bundle instead (Offline installation) |
TOOL_MAX_WORKERS |
Per-job cap on tools running at once, applied only when parallel execution is on (default: 2) |
MAX_LOG_OUTPUT_BYTES |
Cap on stored stdout/stderr per task result (default: 50 KB) |
HUEY_QUEUE_EXPIRY |
Seconds a queued task may wait before it is discarded unstarted (default: 1800). Not an execution timeout. HUEY_TASK_TIMEOUT is a deprecated alias |
WORKER_NAME |
Optional label shown on /admin/workers |
WORKER_IP |
Optional address shown on /admin/workers (use the WireGuard address for remote workers) |
WORKER_HEARTBEAT_TTL |
Seconds before a job's heartbeat expires in Redis (default: 60) |
WORKER_HEARTBEAT_INTERVAL |
Seconds between heartbeat refreshes (default: 30) |
WORKER_ALIVE_TTL |
Seconds before a silent worker disappears from /admin/workers (default: 180) |
MAX_PARSE_EVENTS |
Matched events read from one job's raw output per analytics pass; bounds worker CPU and process-tree memory (default: 250000) |
JOB_OUTPUT_RETENTION_DAYS |
Age at which a job's raw tool outputs are deleted, by the daily sweep and by the Output cleanup action; 0 keeps them forever (default: 90). See Storage and retention |
Config paths¶
| Variable | Description |
|---|---|
ANALYTICS_FIELDS_PATH |
Entity field-name YAML used by analytics (default: config/analytics_fields.yaml) |
THREAT_DETECTION_CONFIG_PATH |
Threat-detection heuristic patterns YAML (default: config/threat_detection.yaml) |
DEPLOY_* reference¶
The fleet deploy tooling (./logstotal deploy, upgrade and the deploy:* commands) does
not read .env. It reads DEPLOY_* and FLEET_* variables from deploy.env or from the
environment of the command. Every one of them, with its default and where it is meant to be
set, is in the Fleet reference; FLEET_FROM and
the other FLEET_* variables are in
Taking the record somewhere else.
Related: Security · Scaling · Prerequisites · Fleet reference · Docs index