Environment Variables Reference
Complete overview of all environment variables for Eviworx. Most variables are defined directly in docker-compose.yaml.
All variables marked "CHANGE!" contain default values that are publicly known. Change them BEFORE production deployment!
Backend Service (Node.js API)
| Variable | Default Value | Description | Change? |
|---|---|---|---|
DATABASE_URL |
postgresql://helpdesk_user:supersecretpassword@db:5432/helpdesk_db |
PostgreSQL connection string | CHANGE! |
JWT_SECRET |
your-secret-key-change-in-production |
JWT token signing (min. 32 bytes) | CHANGE! |
JWT_SECRET_OLD |
Not set | Old JWT secret for secret rotation (optional) | Optional |
SHARE_SECRET |
2b9f3c... |
Signs time-limited public share links. REQUIRED — backend boots with a fatal error if missing! | REQUIRED! |
SHARE_SECRET_OLD |
Not set | Old share secret for zero-downtime rotation (optional) | Optional |
INTERNAL_API_KEY |
2f568de4d1d8627d5a5cdca3890b7d8fa9953e6ebd67aad1baa5d399547b2564 |
Authentication for Worker → Backend (32 bytes hex) | CHANGE! |
LICENSE_ENCRYPTION_KEY |
46a0bb175f00dadf828a90042bfbb3cada81b385f73162c66059a32035826f0a |
AES-256-GCM for encrypted license keys (EXACTLY 32 bytes!) | CHANGE! |
TWO_FACTOR_ENCRYPTION_KEY |
8b2e7f4a1c9d3e6f... |
AES-256-GCM for 2FA secrets (separate key, NOT same as JWT_SECRET!) | CHANGE! |
ADMIN_INITIAL_PASSWORD |
ChangeMeNowXx |
Initial admin password (ONLY on 1st start with empty DB!) | On 1st start! |
FRONTEND_URL |
https://helpdesk.example.com,http://localhost:5173 |
CORS allowed URLs (comma-separated) | Adjust |
NODE_ENV |
production |
Node environment (production / development) | - |
SEED_DATABASE |
true |
Runs the seeding on start: on an empty database the system and admin user (admin@company.com), plus missing default data such as email templates, SLA policies and categories. Existing data is skipped. Set to false after first start! | After 1st start! |
SESSION_MAX_HOURS |
12 |
Maximum session duration in hours | Optional |
ACCESS_TOKEN_EXPIRY_MINUTES |
60 |
Access token validity in minutes | Optional |
REFRESH_TOKEN_EXPIRY_MINUTES |
100 |
Refresh token validity in minutes (details: section "Session & Token Configuration"). | Optional |
IDLE_TIMEOUT_MINUTES |
30 |
Inactivity limit in minutes. The value is handed to the client via sessionConfig and enforced there. | Optional |
COOKIE_SECURE |
true in production |
Secure flag of the auth cookies. Without it, NODE_ENV decides; set explicitly when a production deployment deliberately runs over HTTP (internal network). | Optional |
UPLOAD_DIR |
/app/uploads |
Container path for uploads | - |
QUARANTINE_DIR |
/app/quarantine |
Container path for infected files — dedicated Docker volume, isolated from the uploads volume (ClamAV cannot see it). Set via env/deploy only, NOT in the admin UI. | - |
MAX_FILE_SIZE |
104857600 |
Max upload size (100 MB in bytes) | Optional |
FILE_UPLOAD_RATE_LIMIT |
200 |
Max uploads per hour per IP (further rate limits: see the dedicated section below) | Optional |
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis connection (with password!) | Change password! |
EMAIL_AUTO_CREATE_USER_DAILY_LIMIT |
100 |
Daily limit for auto-created users via inbound email | Optional |
TURNSTILE_SITE_KEY |
0x4AAAAAACX7uYnWmiRmC9TB |
Cloudflare Turnstile site key (bot protection) | Adjust |
TURNSTILE_SECRET_KEY |
0x4AAAAAACX7uTWQ8PjpR_gii... |
Cloudflare Turnstile secret key (bot protection) | CHANGE! |
ENABLE_FIPS |
false |
Enable FIPS mode (FIPS 140-2 compatible algorithms, no certification). The backend checks at startup that Node.js runs in FIPS mode and does not start otherwise. | Optional |
PBKDF2_ITERATIONS |
210000 |
PBKDF2-SHA512 iterations for password hashing | Optional |
UV_THREADPOOL_SIZE |
16 |
Node.js libuv threadpool size (for crypto operations) | Optional |
VAPID_PUBLIC_KEY |
BJ-o94UXnEkgBFI4xpEql84... |
Web push public key (optional) | Optional |
VAPID_PRIVATE_KEY |
y1-b7G-vcsKsPra1_zsJt3Z... |
Web push private key (optional) | Optional |
VAPID_SUBJECT |
mailto:admin@helpdesk.local |
Web push subject (email or URL) | Optional |
TRUSTED_PROXIES |
217.89.98.0/24 |
CIDR ranges of external reverse proxies (comma-separated). Private networks (172.16/10/192.168) are trusted by default. | Optional |
LICENSE_KEY |
EVI-XXXX-XXXX-XXXX |
License key (provided by Eviworx). Without entry the application starts in trial mode. | Optional |
LICENSE_SECRET |
your-license-secret |
License secret for HMAC validation (provided by Eviworx, together with LICENSE_KEY). | Optional |
LICENSE_SERVER_URL |
https://lic.eviworx.com |
License server URL. Only change for air-gapped environments with custom proxy. | Optional |
SSRF_ALLOWLIST |
Empty | Allowed webhook targets: IPv4/IPv6/CIDR/hostname (comma-separated). Applies when a webhook is saved and on every call (job-worker, workflow-engine). Loopback, link-local and cloud metadata addresses are never allowed. | Optional |
SSRF_ALLOWED_PORTS |
80,443,8080,8443 |
Allowed destination ports for webhooks (overrides default). | Optional |
Note: For the backend, company name and application URL are configured in the UI: Admin Center → System → General. The company name in report exports is set by the report generator via its own COMPANY_NAME variable (see below).
Database Service (PostgreSQL)
| Variable | Default Value | Description | Change? |
|---|---|---|---|
POSTGRES_USER |
helpdesk_user |
Main DB user (full access) | - |
POSTGRES_PASSWORD |
supersecretpassword |
Main DB password (MUST match DATABASE_URL!) | CHANGE! |
POSTGRES_DB |
helpdesk_db |
Database name | - |
JOBWORKER_DB_PASSWORD |
jobworker_secure_password_change_me |
Password for helpdesk_jobworker user (restricted access) | CHANGE! |
READONLY_DB_PASSWORD |
readonly_secure_password_change_me |
Password for helpdesk_readonly user (read-only, also for report generator) | CHANGE! |
Important: Passwords are used in init scripts during first DB start to create restricted users. If changed after first start, DB users must be updated manually.
Redis Service
| Variable | Default Value | Description | Change? |
|---|---|---|---|
REDIS_PASSWORD |
7cf9d4a07745c26826a8ad8dc047023decfb95f67acc9d7c |
Redis password (used in --requirepass and all REDIS_URL connections) | CHANGE! |
Important: REDIS_PASSWORD is used in all services as part of REDIS_URL: redis://:PASSWORD@redis:6379. All REDIS_URL values must contain the same password!
Job-Worker Service (CronJobs & Automation)
| Variable | Default Value | Description | Change? |
|---|---|---|---|
DATABASE_URL |
postgresql://helpdesk_jobworker:${JOBWORKER_DB_PASSWORD}@db:5432/helpdesk_db |
Restricted DB user (only CronJob tables) | Change password |
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis connection (with password) | - |
BACKEND_URL |
http://backend:3000 |
Internal API for domain data | - |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend service! | CHANGE! |
NODE_ENV |
production |
Node environment | - |
METRICS_PORT |
3001 |
Internal port for the job worker health check | - |
INSTANCE_ID |
Auto-generated | Custom instance ID for multi-instance setup (optional) | Optional |
Email-Worker Service
| Variable | Default Value | Description | Change? |
|---|---|---|---|
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis for email queue (with password) | - |
REDIS_PASSWORD |
${REDIS_PASSWORD} |
Redis password (must match Redis service) | - |
BACKEND_URL |
http://backend:3000 |
Internal API for SMTP config & templates | - |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend | CHANGE! |
LICENSE_ENCRYPTION_KEY |
46a0bb175f00dadf... |
MUST match backend (for license validation) | CHANGE! |
NODE_ENV |
production |
Node environment | - |
HEALTH_PORT |
3005 |
Health check port | - |
EMAIL_ACCENT_COLOR |
#3b8f93 |
Accent color for email layout | Optional |
EMAIL_APP_NAME |
Eviworx |
App name in email header | Optional |
EMAIL_APP_URL |
Not set | URL for links in email layout | Adjust |
EMAIL_FOOTER_TEXT |
Eviworx 2026 |
Footer text in emails | Optional |
EMAIL_LAYOUT_ENABLED |
true |
Enable/disable email layout | Optional |
FRONTEND_URL |
Not set | Frontend URL for links in emails | Adjust |
EMAIL_INBOUND_RATE_LIMIT_PER_MINUTE |
60 |
Max inbound emails per minute (DDoS protection) | Optional |
EMAIL_INBOUND_RATE_LIMIT_PER_SENDER_PER_HOUR |
30 |
Max inbound emails per sender per hour | Optional |
EMAIL_INBOUND_MAX_SIZE_MB |
25 |
Max email size in MB (prevents memory exhaustion) | Optional |
Note: SMTP/IMAP configuration is NOT set via environment variables, but in the UI: Admin Center → Communication → Email & Mailboxes.
Report-Generator Service
| Variable | Default Value | Description | Change? |
|---|---|---|---|
DATABASE_URL |
postgresql://helpdesk_readonly:${READONLY_DB_PASSWORD}@db:5432/helpdesk_db |
Read-only DB user (SELECT only) | Change password |
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis for report queue (BullMQ) | - |
BACKEND_URL |
http://backend:3000 |
Internal API | - |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend | CHANGE! |
PORT |
3004 |
Internal HTTP port | - |
COMPANY_NAME |
Eviworx Software UG |
Company name in report exports (PDF cover page, footer, document metadata). Independent of the company name in the Admin Center. | Adjust |
CSV_DELIMITER |
; |
CSV delimiter: ";" (German), "," (international), "tab" | Optional |
NODE_ENV |
production |
Node environment | - |
Notification-Worker Service
| Variable | Default Value | Description | Change? |
|---|---|---|---|
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis for notification queue (with password) | - |
REDIS_PASSWORD |
${REDIS_PASSWORD} |
Redis password (must match Redis service) | - |
BACKEND_URL |
http://backend:3000 |
Internal API for users/templates | - |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend | CHANGE! |
NODE_ENV |
production |
Node environment | - |
HEALTH_PORT |
3006 |
Health check port | - |
LOG_LEVEL |
info |
Log level (debug, info, warn, error) | Optional |
Note: Teams (Bot Framework) and Webex are configured in the UI: Admin Center → Communication → Teams resp. Webex.
Workflow-Engine Service
| Variable | Default Value | Description |
|---|---|---|
REDIS_HOST |
redis |
Redis container name |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
${REDIS_PASSWORD} |
Redis password (must match Redis service) |
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis connection string (with password) |
BACKEND_URL |
http://backend:3000 |
Internal API for workflow data |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend |
NODE_ENV |
production |
Node environment |
PORT |
3003 |
Internal HTTP port (health checks) |
LOG_LEVEL |
info |
Log level |
SLA_CHECK_INTERVAL_MINUTES |
5 |
SLA timer check interval (every 5 minutes) |
RECOVERY_STUCK_THRESHOLD_MINUTES |
10 |
Restart stuck workflows after 10 minutes |
CIRCUIT_BREAKER_THRESHOLD |
5 |
Circuit breaker opens after 5 consecutive failures |
CIRCUIT_BREAKER_RESET_MS |
30000 |
Circuit breaker reset after 30 seconds |
AV-Worker Service (Virus Scan Orchestrator)
| Variable | Default Value | Description |
|---|---|---|
BACKEND_URL |
http://backend:3000 |
Internal API for attachment status updates |
INTERNAL_API_KEY |
2f568de4d1d8627d... |
MUST match backend |
CLAMAV_HOST |
clamav |
ClamAV container name |
CLAMAV_PORT |
3310 |
ClamAV TCP port (clamd) |
HEALTH_PORT |
3007 |
Health check port |
REDIS_URL |
redis://:PASSWORD@redis:6379 |
Redis connection (with password) |
SCAN_POLL_CRON |
*/10 * * * * * |
Polling interval for new files (every 10 seconds) |
SCAN_BATCH_SIZE |
5 |
Max files per batch scan |
SCAN_TIMEOUT_MS |
120000 |
Scan timeout per file (2 minutes) |
NODE_ENV |
production |
Node environment |
NODE_OPTIONS |
--max-old-space-size=256 |
Node.js heap limit (256 MB) |
ClamAV Service (Virus Scanner)
| Variable | Default Value | Description |
|---|---|---|
FRESHCLAM_DAEMON |
yes |
Freshclam daemon for automatic signature updates |
CLAMD_DAEMON |
yes |
Start ClamAV daemon (clamd) |
FRESHCLAM_CHECKS |
24 |
Signature update checks per day (every 1 hour) |
Traefik Service (Reverse Proxy)
Traefik does not need environment variables. Configuration is done via YAML files:
| File | Purpose |
|---|---|
./traefik/traefik.yml |
Static configuration (entrypoints, providers, logging, forwardedHeaders.trustedIPs) |
./traefik/dynamic.yml |
Dynamic configuration (routers, services, middleware, TLS) |
./certs/cert.pem |
SSL certificate (fullchain) |
./certs/cert.key |
SSL private key |
External Reverse Proxy: When running behind an external reverse proxy, in addition to TRUSTED_PROXIES (.env), the proxy CIDR must be added to forwardedHeaders.trustedIPs in traefik/traefik.yml for both entrypoints (web + websecure). Installation → External Reverse Proxy
Session & Token Configuration
Session timings are configured exclusively in the backend service. The frontend receives the values from the server at runtime (sessionConfig in the login and /auth/me response).
| Variable | Default Value | Description |
|---|---|---|
SESSION_MAX_HOURS |
12 |
Hard session end (12 hours) — backend-enforced, not extendable even via refresh |
ACCESS_TOKEN_EXPIRY_MINUTES |
60 |
Access token validity (1 hour) |
REFRESH_TOKEN_EXPIRY_MINUTES |
100 |
Refresh token validity. Deliberately tight: it only has to cover the refresh after an idle phase (token then about 60 min old). Longer lifetimes only increase the attack surface for stolen tokens. |
IDLE_TIMEOUT_MINUTES |
30 |
Auto-logout on inactivity. Handed to the client via sessionConfig and enforced there. |
COOKIE_SECURE |
(depends on NODE_ENV) | When set, this value alone determines the secure flag of all auth cookies. If unset, it is enabled in production. |
Rate Limits
All limits are overridable via ENV and are read at startup — a value can therefore be raised during operation (set ENV + docker compose up -d backend) without rebuilding the image. Invalid or non-positive values fall back to the default.
Important to understand: These IP limits are NOT the actual brute-force brake — that sits on the target object (per-email account lockout with exponential backoff, CAPTCHA after 3 failures, per-user 2FA limit, per-email reset limit). The IP values are a safety net against request floods and deliberately generous: behind a corporate NAT, ALL employees share ONE IP — limits that are too tight lock out entire offices there without stopping an attacker.
| Variable | Default | Counts | Description |
|---|---|---|---|
AUTH_FAIL_PER_PAIR_MAX |
5 / 15 min |
IP + email | Failed logins per account AND IP. A colleague with a typo therefore locks only themselves out — not the office. Because account and IP are counted together, nobody can deliberately lock out other people's accounts. |
AUTH_FAIL_PER_IP_MAX |
30 / 15 min |
IP | Failed logins per IP across ALL accounts — a cap against enumerating many email addresses from one source. |
REFRESH_FAIL_PER_IP_MAX |
60 / 15 min |
IP | Failed token refreshes per IP. Refresh tokens are 64 characters and not guessable — this limit is pure DoS protection. Deliberately high: after a deploy that invalidates all sessions, every client in an office hits a failure at the same time. |
LOGOUT_PER_IP_MAX |
120 / 15 min |
IP | Logout calls per IP — counts ALL of them (the endpoint always answers 200). |
TWOFA_FAIL_PER_IP_MAX |
20 / 15 min |
IP | Failed 2FA codes per IP. The real brake is the per-user attempt limit. |
FORGOT_PW_PER_IP_MAX |
20 / 15 min |
IP | Forgot-password requests per IP (per email, an additional 1 request / 2 min applies). |
RESET_PW_PER_IP_MAX |
20 / 15 min |
IP | Failed attempts on password reset and invitation password setup (64-character token). |
OAUTH_CB_PER_IP_MAX |
30 / 15 min |
IP | Failed Entra ID callbacks per IP (successes are not counted). |
INVITE_LOOKUP_PER_IP_MAX |
30 / 15 min |
IP | Invitation token validations per IP — counts all (including valid ones). Onboarding several new hires from one office. |
PASSWORD_CHANGE_MAX |
10 / 15 min |
user | Password changes per user (NOT per IP) — protects the CPU from repeated password hashing. |
CRITICAL_OPS_RATE_LIMIT_MAX |
60 / min |
IP | Write access to user/role/admin endpoints. Counts only changes (POST/PUT/PATCH/DELETE); read access is not counted. |
GLOBAL_RATE_LIMIT_MAX |
2000 / min |
IP | Overall cap across /api/*. Rule of thumb: an office with 50 active agents generates a good 1000 requests/min through polling and refetching — the value needs headroom. |
FILE_UPLOAD_RATE_LIMIT |
200 / h |
IP | Uploads per hour — likewise per office IP; raise it if many attachments are expected. |
In-memory counters: The counters live in the backend's memory; a restart resets them. The backend runs on a single instance, so the values above apply exactly as stated (see Scaling).
Behind an external reverse proxy, always check TRUSTED_PROXIES: without correct client IP resolution ALL requests look like a single IP — then every IP limit immediately applies to everyone.
Generate Secrets - Detailed Guide
JWT_SECRET (Backend)
# Generate 64-char hex string (no special characters, URL-safe)
openssl rand -hex 32
# Example output:
# 9f4a2c8e1b7d5f3a0e6c9d2b4f8a1e3c7d5b9f2a4e6c8d0b3f5a7e9c1d3b5f7a
# Add to .env:
JWT_SECRET=9f4a2c8e1b7d5f3a0e6c9d2b4f8a1e3c7d5b9f2a4e6c8d0b3f5a7e9c1d3b5f7a
SHARE_SECRET (Backend)
# REQUIRED — without this value the backend will NOT start (fatal error)!
# Signs time-limited public share links.
openssl rand -hex 32
# Add to .env:
SHARE_SECRET=generated_hex_string
# Optional for zero-downtime rotation:
# SHARE_SECRET_OLD=old_hex_string
INTERNAL_API_KEY (backend + all workers)
# Generate 32-byte hex string
openssl rand -hex 32
# Example output:
# a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456
# Enter the SAME key everywhere:
# - Backend-Service: INTERNAL_API_KEY=a1b2c3d4...
# - Job-Worker-Service: INTERNAL_API_KEY=a1b2c3d4...
# - Email-Worker-Service: INTERNAL_API_KEY=a1b2c3d4...
# - Notification-Worker-Service: INTERNAL_API_KEY=a1b2c3d4...
# - Workflow-Engine-Service: INTERNAL_API_KEY=a1b2c3d4...
# - AV-Worker-Service: INTERNAL_API_KEY=a1b2c3d4...
# - Report-Generator-Service: INTERNAL_API_KEY=a1b2c3d4...
LICENSE_ENCRYPTION_KEY (Backend + Email-Worker)
# MUST be exactly 32 bytes (64 hex chars)!
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Example output:
# 1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890
# Add to .env (used automatically in Backend + Email-Worker):
LICENSE_ENCRYPTION_KEY=1a2b3c4d5e6f7890...
TWO_FACTOR_ENCRYPTION_KEY (Backend)
# MUST be exactly 32 bytes (64 hex chars)!
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Separate key (NOT same as JWT_SECRET or LICENSE_ENCRYPTION_KEY!)
- TWO_FACTOR_ENCRYPTION_KEY=your_generated_key_here
REDIS_PASSWORD
# Generate a secure Redis password (64 hex chars, no special characters)
openssl rand -hex 32
# Add to .env:
REDIS_PASSWORD=generated_hex_string
# Used automatically in all REDIS_URL connections:
# redis://:PASSWORD@redis:6379
CRITICAL: The LICENSE_ENCRYPTION_KEY must NEVER be changed after licenses are encrypted! Key backup is MANDATORY!
Database Passwords
# Generate 3 different secure passwords (64 hex chars)
openssl rand -hex 32 # → POSTGRES_PASSWORD
openssl rand -hex 32 # → JOBWORKER_DB_PASSWORD
openssl rand -hex 32 # → READONLY_DB_PASSWORD
# Add to .env:
POSTGRES_PASSWORD=generated_password
JOBWORKER_DB_PASSWORD=another_password
READONLY_DB_PASSWORD=third_password
# The DATABASE_URL connections in docker-compose.yaml
# reference these values automatically via ${POSTGRES_PASSWORD} etc.
# report-generator service:
- DATABASE_URL=postgresql://helpdesk_readonly:$PW3@db:5432/helpdesk_db
Advanced Variables
Container Resource Limits
Resource limits are defined in docker-compose.yaml under deploy.resources.limits:
# Example: ClamAV
clamav:
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 512M
Security Hardening (AV-Worker)
The AV-Worker is especially hardened:
av-worker:
read_only: true # Read-only Filesystem
tmpfs:
- /app/tmp:size=64M # Temporary writable space
- /tmp:size=64M
security_opt:
- no-new-privileges:true # No privilege escalation
cap_drop:
- ALL # All Linux capabilities dropped
Logging
Log rotation is configured for all containers:
logging:
driver: "json-file"
options:
max-size: "10m" # Max 10 MB per log file
max-file: "3" # Max 3 log files (total: 30 MB)
Variable Validation
Check All Variables
# Show all environment variables (resolved)
docker compose config
# Check INTERNAL_API_KEY is the same everywhere
docker compose config | grep INTERNAL_API_KEY
# Check DATABASE_URL password match
docker compose config | grep -E "POSTGRES_PASSWORD|DATABASE_URL"
# Check REDIS_PASSWORD is the same everywhere
docker compose config | grep -E "REDIS_PASSWORD|REDIS_URL"
Common Mistakes
| Error | Symptom | Solution |
|---|---|---|
| INTERNAL_API_KEY different | Workers cannot communicate with backend (401) | Check all workers have same key |
| DB password mismatch | Backend cannot connect to DB | POSTGRES_PASSWORD === DATABASE_URL password |
| REDIS_PASSWORD mismatch | Services cannot connect to Redis (NOAUTH) | Check REDIS_PASSWORD is identical everywhere |
| LICENSE_ENCRYPTION_KEY wrong length | License key encryption fails | MUST be exactly 32 bytes (64 hex chars) |
| SEED_DATABASE stays true | Seeding runs on every restart; missing default data (e.g. categories, SLA policies) is recreated | Set to false after first start |
Production Deployment Checklist
| Category | Variable | Status |
|---|---|---|
| Secrets | JWT_SECRET |
Changed |
SHARE_SECRET |
Set (REQUIRED!) | |
INTERNAL_API_KEY |
Changed & identical everywhere | |
LICENSE_ENCRYPTION_KEY |
Changed (32 bytes!) & backed up | |
TWO_FACTOR_ENCRYPTION_KEY |
Changed (32 bytes!) | |
| Database & Redis | POSTGRES_PASSWORD |
Changed |
JOBWORKER_DB_PASSWORD |
Changed | |
READONLY_DB_PASSWORD |
Changed | |
REDIS_PASSWORD |
Changed & identical everywhere | |
| Configuration | FRONTEND_URL |
Set to real domain |
TURNSTILE_SITE_KEY / SECRET_KEY |
Own Cloudflare keys set | |
| Post-Install | SEED_DATABASE |
Set to false after first start |
| Default user passwords | All changed in UI |
Optional Variables
JWT Secret Rotation
For JWT secret rotation (zero-downtime) you can use JWT_SECRET_OLD:
# Current secret: JWT_SECRET=old_key_123
# Step 1: Add the new key
- JWT_SECRET=new_key_456
- JWT_SECRET_OLD=old_key_123 # Old key stays valid
# Step 2: Wait 24h (until all old tokens have expired)
# Step 3: Remove JWT_SECRET_OLD
# - JWT_SECRET_OLD=... # Comment out or delete
VAPID Keys (Web-Push-Notifications)
If you want to use browser push notifications:
# 1. Install the web-push CLI
npm install -g web-push
# 2. Generate a VAPID key pair
web-push generate-vapid-keys
# 3. Add to docker-compose.yaml (backend service):
- VAPID_PUBLIC_KEY=BJ-o94UXnEkgBFI4xpEql84...
- VAPID_PRIVATE_KEY=y1-b7G-vcsKsPra1_zsJt3Z...
- VAPID_SUBJECT=mailto:admin@your-domain.com
Custom Instance ID (Multi-Instance)
# For multi-instance deployments (HA)
# Job-Worker Service:
- INSTANCE_ID=job-worker-prod-1
# Useful for:
# - Monitoring (unique identification)
# - Logging (which instance ran the job)
# - Debugging
Override Variables via .env File
You can create a .env file in the root directory to override variables:
# Create .env in the root directory
cat > .env << 'EOF'
# Custom Overrides
FRONTEND_URL=https://your-domain.com
JOBWORKER_DB_PASSWORD=your_secure_password_here
READONLY_DB_PASSWORD=another_secure_password
REDIS_PASSWORD=your_redis_password_here
ADMIN_INITIAL_PASSWORD=your_initial_admin_password
COMPANY_NAME=Your Company Ltd
EOF
# Docker Compose reads .env automatically
docker compose up -d
Security: Add .env to .gitignore! Never commit secrets to Git.
Check Variables at Runtime
In Running Container
# Show backend environment
docker compose exec backend env | grep -E "JWT_SECRET|DATABASE_URL|INTERNAL"
# Job-Worker environment
docker compose exec job-worker env
# Compare all container environments
for service in backend job-worker email-worker notification-worker workflow-engine av-worker report-generator; do
echo "=== $service ==="
docker compose exec $service env | grep INTERNAL_API_KEY
done
Before Start (Dry-Run)
# Show the complete resolved config (without starting)
docker compose config
# Show only the backend service config
docker compose config backend
# Validate docker-compose.yaml
docker compose config --quiet && echo "Valid YAML" || echo "Invalid YAML"
Best Practices
1. Never Hardcode Secrets
- Secrets directly in docker-compose.yaml (bad for Git) - AVOID
- Secrets in .env file (not in Git!) - RECOMMENDED
- Or: secrets from an orchestrator (e.g. Kubernetes), passed in as environment variables
2. Keep INTERNAL_API_KEY in Sync
The INTERNAL_API_KEY MUST be identical in all 7 services:
- backend
- job-worker
- email-worker
- notification-worker
- workflow-engine
- av-worker
- report-generator
Tip: Use a .env variable:
# In .env:
INTERNAL_API_KEY=your_generated_key_here
# In docker-compose.yaml (all services):
environment:
- INTERNAL_API_KEY=${INTERNAL_API_KEY}
3. Backup LICENSE_ENCRYPTION_KEY
The LICENSE_ENCRYPTION_KEY encrypts all license keys in the database. WITHOUT this key, licenses are NOT decryptable!
# Backup strategy:
# 1. Store the key in a password manager (1Password, Bitwarden, etc.)
# 2. Offline backup (paper wallet)
# 3. Encrypted backup on a separate server
# NEVER:
# - Store only in docker-compose.yaml
# - Commit to Git
# - Send via email
4. Set Session Times
Backend only — the frontend fetches the values from the server at runtime:
# In docker-compose.yaml (backend):
- SESSION_MAX_HOURS=12
- ACCESS_TOKEN_EXPIRY_MINUTES=60
- REFRESH_TOKEN_EXPIRY_MINUTES=100
- IDLE_TIMEOUT_MINUTES=30
# frontend: no session variables needed
Back to installation guide
Detailed docker-compose.yaml explanation