Eviworx
Docs

Environment Variables Reference

Complete overview of all environment variables for Eviworx. Most variables are defined directly in docker-compose.yaml.

⚠️
Secrets MUST be changed!

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
Next Steps
← Installation

Back to installation guide

Docker Compose Details →

Detailed docker-compose.yaml explanation