Configuration
.env for the infrastructure, the console for the rest.
Every environment variable the server reads, what it does, its default and where the value comes from. Most product settings can also be changed in the operator console without a restart.
01Where settings live
| Place | What goes there | How a change applies |
|---|---|---|
.env in the kestrel folder (Docker install) | Everything below. install.sh writes it; docker-compose.yml passes it to the server (env_file: .env). | Edit, then docker compose up -d. A change of PUBLIC_URL needs ./update.sh, because the web app is rebuilt with the new address. |
server/.env (developer setup) | The same variables, for a server you run with Node.js | Restart the server |
Operator console /admin | Brand, legal, access, tenancy, features, plans and limits, broker ids, AI, delivery, integrations, notifications | At once, no restart. A value saved in the console wins over .env (Operator console). |
The reference files in the package are .env.example (Docker), server/.env.example and server/.env.ai.example. In an .env file, KEY= with nothing after it means "not set".
02Core and infrastructure
These are read once at start and stay in .env (server/src/config.ts, docker-compose.yml).
| Variable | Default | What it does, where the value comes from |
|---|---|---|
KESTREL_MASTER_KEY Required | none | 32 random bytes, base64. Encrypts every stored exchange key. Create it with openssl rand -base64 32; the installer does it for you. Losing it makes every stored key unreadable (Master key). The server refuses to start without it or when it does not decode to 32 bytes. |
DATABASE_URL | sqlite:./data/kestrel.db (Docker: sqlite:/data/kestrel.db) | sqlite:<path> for one box, or postgres://user:pass@host:5432/kestrel. Migrations run at start. |
PUBLIC_URL | http://localhost:8080 | The address people open, e.g. https://trade.example.com. Webhook URLs of signal bots use it, and the Docker build compiles it into the web app. |
CORS_ORIGINS | * | Comma-separated origins allowed to call the API from a browser. Set it to your PUBLIC_URL; the installer does. |
WS_ORIGINS | same as CORS_ORIGINS | Origins allowed to open the WebSocket. Apps that send no Origin header (the mobile and desktop apps) always pass. |
SERVER_PUBLIC_IP | empty | The server's public IP, shown to users at /api/server-ip and in the app so they can whitelist it on the exchange. The installer detects it. |
TRUST_PROXY_HOPS | 0 (installer: 1) | Number of reverse proxies in front of the server. Caddy in the Docker install is 1. Rate limits and the audit log use the client IP found this way. |
PORT | 8080 | Port the server listens on. Docker fixes it to 8080 inside the container. |
RUNNER_INTERVAL_MS | 2000 | How often the Smart Trade and bot runners make a pass, in milliseconds. The watchdog calls a runner stalled after 3 intervals without a pass. |
NODE_ENV | production | Usually left alone. |
Docker only
| Variable | What it does |
|---|---|
DOMAIN | Your domain, written by the installer and reused when you run it again. |
SITE_ADDRESS | The address Caddy serves: your domain (automatic HTTPS) or :80 (plain HTTP, no domain). |
ACME_EMAIL | Email given to the certificate authority for expiry notices. |
COMPOSE_PROFILES | postgres starts the PostgreSQL container. |
POSTGRES_PASSWORD | Password of the PostgreSQL container, random from the installer. |
03Operator console
| Variable | Default | What it does |
|---|---|---|
ADMIN_SECRET_KEY | derived from KESTREL_MASTER_KEY | Optional. 32 bytes as 64 hex characters (openssl rand -hex 32). Encrypts the console's stored secrets (bot token, SMTP URL, API keys). Changing it later makes those stored secrets unreadable: enter them again. |
ADMIN_DEMO_PASSWORD | empty | Only on a public demo host. Makes the console read-only with the account demo@kestrel.demo and this password, and the Users page refuses every change. Never set it on a real install. ADMIN_DEMO_EMAIL and ADMIN_DEMO_NOTICE change the demo account and its banner. |
04Services: Telegram, email, push
Each can be set in the console (Integrations, owner only) instead. A value saved in the console wins; these are the fallback (server/.env.example).
| Variable | What it does, where to get it |
|---|---|
TELEGRAM_BOT_TOKEN | Your own bot's token, from BotFather in Telegram. Turns on Telegram notifications, channel signals and the control commands (Create the bot). |
SMTP_URL | smtps://user:pass@host:465 or smtp://user:pass@host:587 (STARTTLS is required on 587). From your email provider's SMTP settings. Unset: no email. |
MAIL_FROM | The sender address, e.g. KESTREL <no-reply@example.com>. Required with SMTP; the domain must be allowed to send through your provider. |
FCM_SERVICE_ACCOUNT_FILE | Path to the Firebase service account JSON file, for push. In Docker the path must be inside the container, so paste the JSON in the console (Integrations) instead (Push). |
A Telegram bot is free. Firebase Cloud Messaging is free. SMTP providers usually have a free tier with a sending limit and charge above it.
05Branding and legal
The starting values of the console's Brand and Legal sections, published at GET /api/config.
| Variable | Default | What it does |
|---|---|---|
BRAND_NAME | KESTREL | Product name, up to 60 characters. |
BRAND_ACCENT | #FF6B35 | Accent colour, #RRGGBB. |
BRAND_LOGO_URL | empty | Logo URL. |
LEGAL_TERMS_URL, LEGAL_PRIVACY_URL | empty | Your terms of service and privacy policy. |
The compiled apps take their own name, colours and icon from the app source; see White label.
06Feature switches
Everything is on by default. KESTREL_FEATURE_<NAME>=false (also 0, off, no) is the starting value of the matching switch in the console's Features section (server/src/features.ts).
Names: JOURNAL, ANALYTICS, EXPORT, ALGO, COPY, SCANNER, AI, TELEGRAM, PUSH, EMAIL, PANIC, RISK. Example: KESTREL_FEATURE_COPY=false.
A switched-off feature disappears from the app's navigation and its API routes answer 404 feature_disabled. Paper trading, Smart Trade, bots and alerts cannot be switched off. Switching off EMAIL only hides the flag: sign-in and email verification keep working.
07Copy trading, scanner, AI assistant
From server/.env.ai.example. Every one has a safe default.
| Variable | Default | What it does |
|---|---|---|
KESTREL_TENANCY | self_hosted | self_hosted or saas. Starting value of the console's Tenancy section (Tenancy). |
COPY_INTERVAL_MS | 3000 | How often the copy runner reads masters and converges followers. Minimum 500. |
SCANNER_DEFAULT_TAKER_FEE | 0.001 | Taker fee rate the spread scanner uses when a market reports none (shown as "default"). |
SCANNER_RATE_PER_MIN | 30 | Scanner requests per user per minute. |
KESTREL_AI_PROVIDER | empty (off) | anthropic or openai-compatible (OpenAI, OpenRouter, Ollama, vLLM and others). |
KESTREL_AI_BASE_URL | the provider's own | Optional. Defaults to https://api.anthropic.com or https://api.openai.com/v1. For a local Ollama: http://localhost:11434/v1 (from inside Docker, the address of the machine running Ollama). |
KESTREL_AI_MODEL | empty | The model name, as your provider writes it. |
KESTREL_AI_API_KEY | empty | Your provider's API key. Never logged, never returned by any endpoint. Can be set in the console (Integrations, LLM API key) instead. |
KESTREL_AI_RATE_PER_HOUR | 20 | AI requests per user per hour. |
KESTREL_AI_TIMEOUT_MS | 30000 | How long the server waits for the provider. Minimum 1000. |
The assistant turns on only when provider, model and key are all set. Without them every AI route answers 501 ai_not_configured and the app hides the assistant.
Hosted models are paid per use (per token) to the provider, on your account. Limit the cost with KESTREL_AI_RATE_PER_HOUR and the per-plan AI limit. A local model through Ollama has no per-use fee.
08Changing a value
cd kestrel
nano .env # edit, save with Ctrl+O, exit with Ctrl+X
docker compose up -d # recreates the server with the new values
Changed your domain? Edit PUBLIC_URL, SITE_ADDRESS, CORS_ORIGINS, DOMAIN (and ACME_EMAIL), then run ./update.sh so the web app is rebuilt for the new address.