Installation & setup guide
Everything you need to get SecuriReport AI running against your own Nginx logs, from a bare Node.js install through to Docker, configuration, and licensing. This app is a self-hosted, single-tenant tool — whoever installs it configures it; there's no separate admin-versus-end-user split.
Prerequisites
| Requirement | Needed for |
|---|---|
| Node.js 22+ | Running locally without Docker |
| Docker & Docker Compose | Running in a container (recommended for production) |
| An Nginx access/error log file | Log ingestion — a local path or a mounted volume |
| A running Ollama instance (optional) | Local, offline AI report generation |
| A Gemini, OpenAI, and/or Anthropic API key (optional) | Cloud AI report generation |
| A license key from your purchase email | Clearing the "Unlicensed" warning banner |
You don't need Ollama or any cloud API key to run the app. Without either configured, report generation automatically falls back to a deterministic, template-based engine — you always get a report.
Install with Node.js
-
Install dependencies
From the project root:
npm install npm run devThis starts the Express backend (
server.ts, viatsx), which also serves the frontend. By default the app listens on port3000(configurable viaPORTonce you build for production — see the note below). -
Build for production (optional)
npm run build # builds the Vite frontend and bundles the server npm start # runs the bundled server (dist/server.cjs)
Install with Docker
-
Copy the environment file
cp .env.example .env -
Fill in the required security keys
At minimum, set
APP_API_KEYandVITE_APP_API_KEY— see Configuration below. Both are required before going to production. -
Point at your real log file
Set
NGINX_LOG_PATH(and optionallyNGINX_ERROR_LOG_PATH) to your actual log file or a mounted volume. -
Build and start the container
docker compose build # required after changing VITE_APP_API_KEY or the Dockerfile docker compose up -dThe container binds to
127.0.0.1:3015on the host by default — not0.0.0.0— because it's designed to sit behind your own reverse proxy (Nginx, HAProxy, Caddy) with TLS, rather than face the internet directly.
Rebuild, don't just restart, after changing VITE_APP_API_KEY. It's baked into the frontend bundle at build time, not read at runtime — docker compose build is required, not just up.
The container always listens on port 3000 internally, regardless of the PORT variable. The host-facing port comes from the ports: mapping in docker-compose.yml (default 127.0.0.1:3015:3000) — change the left-hand side there for a different host port. The default log-directory mount is read-only, which is intentional (see Troubleshooting).
Configuration reference (.env)
All configuration lives in a single .env file at the project root. The variables below are the ones worth a real decision before going live; every other variable has a sensible default.
Log source
NGINX_LOG_PATH="/logs/access.log"
NGINX_ERROR_LOG_PATH="/logs/error.log"
Local AI (Ollama)
OLLAMA_BASE_URL="http://host.docker.internal:11434"
OLLAMA_MODEL="qwen2.5-coder:1.5b"
If Ollama runs on the Docker host itself, host.docker.internal is the correct hostname from inside the container. Only use instruction/chat-tuned models — base code-completion models were never trained to follow a system prompt and will produce off-topic output.
Cloud AI providers (all optional, independently configurable)
GEMINI_API_KEY="..."
OPENAI_API_KEY="..."
ANTHROPIC_API_KEY="..."
Any combination may be set. Each one becomes a selectable engine in the Report Generator's picker — you're never forced to enable all three.
Security — required in production
APP_API_KEY="change-me-generate-a-random-secret"
VITE_APP_API_KEY="change-me-generate-a-random-secret"
These two must be identical. Generate one with:
openssl rand -hex 32
APP_API_KEY is checked via the X-API-Key header on every state-changing route (log config, test-traffic injection, Ollama config, AI report generation, AI threat analysis). If unset, the server refuses those routes with a 503 in production.
ALLOWED_LOG_DIRS=""
OLLAMA_ALLOWED_HOSTS=""
Allow-lists that harden /api/logs/config (against arbitrary file reads) and /api/ollama/config (against SSRF). Leave blank unless you need to widen where these can point.
| Variable | Default | Description |
|---|---|---|
PORT | 3000 | Documented but not read at runtime in Docker — use the ports: mapping instead. |
TRUSTED_ADMIN_IPS | empty | Seeds the Firewall tab's whitelist on a browser's first-ever load. UI convenience only, not access control. |
LOG_LEVEL | info | debug logs connect time, first-byte latency and chunk counts per Ollama request. |
OLLAMA_IDLE_TIMEOUT_MS | 240000 | Raise this on CPU-constrained hosts — prompt prefill can take well over a minute before the first token streams. |
OLLAMA_HARD_CEILING_MS | 600000 | Absolute cap on one report generation, regardless of activity. |
OLLAMA_NUM_PREDICT | 900 | Max tokens generated per report. |
AUTH_USERS | disabled | Comma-separated username:password pairs — see Optional login screen. |
Licensing & activation
This is a paid product, licensed and fulfilled entirely through Lemon Squeezy's built-in License Keys feature — there's no custom account system, webhook, or login screen involved on either side.
-
Buy the license
From this site's pricing section, which redirects to Lemon Squeezy checkout.
-
Check your inbox
Lemon Squeezy emails you a license key automatically as part of your purchase receipt — no code on our side is involved in issuing it.
-
Set the key in your .env file
APP_LICENSE_KEY="<the key from your receipt email>" -
Restart the app
docker compose up -d --build # or, if running locally: npm start
There's no in-app "enter license key" screen — the key is a deploy-time configuration value, exactly like APP_API_KEY.
How you'll know it worked
| Signal | Meaning |
|---|---|
Server log: [license] Valid license for <email> | License accepted |
| Green "Licensed" badge next to Logout in the header | License is currently valid |
| Red pulsing "Unlicensed" badge + a red banner | Key is missing, invalid, or belongs to a different product |
This is a soft check, not a hard lock. The app keeps working fully either way — running unlicensed simply means you're operating outside the terms of the license agreement.
Network access is required for licensing. At startup, and again every 24 hours, the app calls Lemon Squeezy's License API (api.lemonsqueezy.com). This is not an air-gapped/offline-capable feature.
Persist LICENSE_STATE_PATH. The first successful check creates an "activation instance" on Lemon Squeezy's side and caches its ID locally (default ./data/license-instance.json, mounted as a volume in docker-compose.yml). If that file is lost, every restart activates a brand-new instance instead of re-validating the existing one, and you'll eventually hit your license's activation limit. Don't remove the ./data:/app/data volume mount without understanding this.
Optional: the login screen
By default, the app has no login screen — anyone who can reach it on the network can use it. That's intentional: it's designed as an internal operator tool sitting behind your own network-level access control (an Nginx allow-list, a cloud firewall, a VPN), not as a public-facing service.
To add a login layer on top of that, set in .env:
AUTH_USERS="alice:secret1,bob:secret2"
and restart. This turns on a custom login screen, a signed HttpOnly session cookie with a 12-hour lifetime, and a Logout button in the header. Passwords are compared with a constant-time comparison to avoid leaking information via timing. This does not encrypt traffic itself — rely on it only over HTTPS, alongside your existing network-level controls, not instead of them.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
| Red "Unlicensed" banner won't go away | APP_LICENSE_KEY missing, invalid, wrong product, or the app can't reach api.lemonsqueezy.com. Check the [license] ... line in startup logs. |
| State-changing actions fail with a 503 | APP_API_KEY / VITE_APP_API_KEY not set, or the two values don't match. Rebuild after changing them in Docker. |
| "Inject Test Traffic" does nothing | The configured log path isn't writable — expected with the default read-only Docker mount. Point NGINX_LOG_PATH at a writable path if you need this. |
| A cloud AI provider doesn't appear in the engine picker | Its API key isn't set in .env, or the app wasn't restarted after setting it. |
| Report generation via Ollama times out or hangs | Raise OLLAMA_IDLE_TIMEOUT_MS — cold model loads on CPU-constrained hosts can take well over a minute before the first token streams back. |
| Report generation silently falls back to the deterministic template | The selected AI provider errored out (bad model, unreachable host, rate limit). This is the designed fail-safe, not a bug — check LOG_LEVEL=debug logs for the underlying error. |
| Can't reach the app at all in Docker | Check you're hitting 127.0.0.1:3015 or your reverse proxy — the container doesn't bind 0.0.0.0 by default. |
| Login screen appears unexpectedly, or doesn't appear when expected | Controlled entirely by whether AUTH_USERS is set. Check .env and restart after any change. |
Still stuck? This is a documentation-only license with no dedicated support line — but the FAQ and the assistant in the bottom-right corner of this site can point you to the right section.