Eviworx
Docs

Docker Compose Setup

Eviworx ITSM consists of 12 Docker containers for production. This page describes each service with networking, startup dependencies (depends_on), health checks, volumes and hardening.

🐳
Container Overview
Reverse Proxy Layer:
  • 1. traefik - Reverse Proxy (Traefik v3.7)
Frontend Layer:
  • 2. frontend - React App (NGINX)
Backend Layer:
  • 3. backend - Node.js API
Data Layer:
  • 4. db - PostgreSQL 17
  • 5. redis - Redis 8 (Cache/Queue)
Worker Layer:
  • 6. email-worker - Email Processing
  • 7. job-worker - CronJobs
  • 8. workflow-engine - Workflows
  • 9. notification-worker - External APIs
  • 10. report-generator - Reports & CSV Export
Security Layer:
  • 11. clamav - Virus Scanner
  • 12. av-worker - Scan Worker

Service Details

1. traefik (Reverse Proxy)

Purpose: Reverse proxy, TLS termination, load balancing

Technology:
• Traefik v3.7.7
• Port 80 (HTTP → HTTPS Redirect)
• Port 443 (HTTPS)

Configuration:
• No environment variables needed (configuration via YAML files)
• traefik.yml (static config, read-only, incl. forwardedHeaders.trustedIPs)
• dynamic.yml (dynamic config, read-only)
• SSL certificates (cert.pem + cert.key, read-only)

External Reverse Proxy:
• When behind external proxy: configure forwardedHeaders.trustedIPs in traefik.yml + TRUSTED_PROXIES in .env
Security Hardening:
• security_opt: no-new-privileges:true

Health Check:
• traefik healthcheck --ping --ping.entrypoint=ping
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 10s

Depends On:
• backend (service_healthy)
• frontend (service_healthy)

Restart Policy:
• unless-stopped

2. frontend (React App)

Purpose: React 19 single-page application served by NGINX

Technology:
• React 19+ with TypeScript
• Vite Build-System
• NGINX (Production Webserver)
• No external port (only expose: 80, access via Traefik)
Health Check:
• wget -qO- http://localhost:80/health
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 10s

Security Hardening:
• read_only: true (read-only filesystem, tmpfs for /var/cache/nginx, /var/run, /tmp)
• security_opt: no-new-privileges:true
• cap_drop: NET_RAW, SYS_ADMIN, MKNOD

Volumes:
• sourcemaps:/opt/sourcemaps:rw (stores the .map files of its own release on start and keeps the five newest releases; backend reads them read-only)

Depends On:
• backend (service_healthy)

Restart Policy:
• unless-stopped

3. backend (Node.js API)

Purpose: REST API, business logic, RBAC, internal API for the workers

Technology:
• Node.js 20+ with TypeScript
• Express.js Framework
• Prisma ORM (PostgreSQL)
• Port 3000

Key Environment Variables:
• DATABASE_URL: Full DB-Access (helpdesk_user)
• JWT_SECRET: For token signing (CHANGE!)
• SHARE_SECRET: Signs public share links — REQUIRED, backend won't boot without it (CHANGE!)
• INTERNAL_API_KEY: For worker access (CHANGE!)
• LICENSE_ENCRYPTION_KEY: AES-256 Key (NEVER change!)
• TWO_FACTOR_ENCRYPTION_KEY: Separate key for 2FA secrets
• ADMIN_INITIAL_PASSWORD: Initial admin password (only on 1st start)
• REDIS_URL: redis://:PASSWORD@redis:6379 (with password!)
• SEED_DATABASE: true on first start
• SESSION_MAX_HOURS, ACCESS_TOKEN_EXPIRY_MINUTES, REFRESH_TOKEN_EXPIRY_MINUTES
• ENABLE_FIPS, PBKDF2_ITERATIONS, UV_THREADPOOL_SIZE
• TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY (Cloudflare Bot Protection)
• FILE_UPLOAD_RATE_LIMIT, EMAIL_AUTO_CREATE_USER_DAILY_LIMIT

Configured in the UI:
• Company name and application URL (Admin Center → System → General)
Health Check:
• node fetch http://localhost:3000/api/health/live
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 300s (for migrations + seed)

Volumes:
• uploads:/app/uploads (Attachments)
• quarantine:/app/quarantine (Infected Files)
• sourcemaps:/opt/sourcemaps:ro (reads frontend source maps for error tracking)

Depends On:
• db (service_started)
• redis (service_started)

4. db (PostgreSQL 17)

Purpose: Persistent database for all application data

Technology:
• PostgreSQL 17 Alpine (small image)
• No external port (only expose: 5432, internal only)
Environment Variables:
• POSTGRES_USER: helpdesk_user (Main User, Full-Access)
• POSTGRES_PASSWORD: supersecretpassword (CHANGE!)
• POSTGRES_DB: helpdesk_db
• JOBWORKER_DB_PASSWORD: Restricted User-Password (CHANGE!)
• READONLY_DB_PASSWORD: Read-Only User-Password (CHANGE!)

DB User Hierarchy:

1. helpdesk_user (Main)
   └─ Full-Access, Migrations, Schema-Changes
   └─ Only backend uses this user
2. helpdesk_jobworker (Restricted)
   └─ Only CronJob, JobExecution, WorkerInstance
   └─ Principle of Least Privilege
   └─ Job-Worker uses this user
3. helpdesk_readonly (Read-Only)
   └─ SELECT on all tables
   └─ For reporting/analytics & report generator
Health Check:
• pg_isready -U helpdesk_user -d helpdesk_db
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Init Scripts:
• Baked into image (01-create-users.sh creates restricted users)• Run only on FIRST start (when DB empty)
Volumes:
• postgres_data:/var/lib/postgresql/data (Persistence!)

Restart Policy:
• unless-stopped

5. redis (Cache & Queue)

Purpose: Cache, pub/sub (workflows), queues (email/notifications), locks

Technology:
• Redis 8.6 Alpine
• No external port (only expose: 6379, internal only)
• Appendonly-Mode (Persistence)
• Password-protected (--requirepass)

Use Cases:Cache: Session-Cache, Query-Cache
• PubSub: workflow:step:complete Events
• Queue: Email-Queue, Notification-Queue, Report-Queue
• Locks: Distributed Locks (CronJobs, Workflows)
• Audit-Fallback: On DB failure (7 day TTL)
Command:
• redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}

Health Check:
• redis-cli -a ${REDIS_PASSWORD} --no-auth-warning ping | grep PONG
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 10s

Volumes:
• redis_data:/data (AOF-Files)

Restart Policy:
• unless-stopped

6. email-worker (Email Processing)

Purpose: Background email processing (SMTP sending, templates, i18n)

Technology:
• Node.js 20+
• BullMQ (Redis-Queue)
• Nodemailer (SMTP-Client)
• Handlebars (Templates)
• Health-Port: 3005

Architecture:NO database connection
• All data via Backend Internal API
• SMTP-Config via Backend-API
• Template-Rendering via Backend-API

Environment Variables:
• REDIS_URL: redis://:PASSWORD@redis:6379 (with password)
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: Authentication for Backend API
• LICENSE_ENCRYPTION_KEY: For license validation
• HEALTH_PORT: 3005
• EMAIL_ACCENT_COLOR, EMAIL_APP_NAME, EMAIL_APP_URL (Branding)
• EMAIL_FOOTER_TEXT, EMAIL_LAYOUT_ENABLED (Layout)
• FRONTEND_URL (For links in emails)
• EMAIL_INBOUND_RATE_LIMIT_PER_MINUTE (DDoS protection)
• EMAIL_INBOUND_RATE_LIMIT_PER_SENDER_PER_HOUR
• EMAIL_INBOUND_MAX_SIZE_MB (Max email size)

Health Check:
• node dist/healthcheck.js
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Depends On:
• redis (service_healthy)
• backend (service_healthy)

Restart Policy:
• unless-stopped

7. job-worker (CronJobs & Automation)

Purpose: Scheduled jobs (28 action types), SLA monitor, asset clustering

Technology:
• Node.js 20+
• node-cron (Scheduler)
• BullMQ (Queue)
• Port 3001 (Health-Check)

Architecture:RESTRICTED DB-User: helpdesk_jobworker
• Access only to: CronJob, JobExecution, WorkerInstance
• All other data via Backend Internal API
• Principle of Least Privilege

Environment Variables:
• DATABASE_URL: postgresql://helpdesk_jobworker:PASSWORD@db/helpdesk_db
• REDIS_URL: redis://:PASSWORD@redis:6379
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: For Backend API
• INSTANCE_ID: Optional (Auto-Generated for multi-instance)

Multi-Instance Support:
• Distributed Locks via Redis (prevents duplicate execution)
• Heartbeat every 15s (entry expires after 30s)
• Execution-Tracking (executedBy-Field)

Health Check:
• node fetch http://localhost:3001/health/live
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Security Hardening:
• read_only: true, tmpfs: /tmp
• security_opt: no-new-privileges:true
• cap_drop: ALL
• No volumes (Prisma schema baked into image)
Depends On:
• db (service_healthy)
• redis (service_healthy)
• backend (service_healthy) ← Important! Backend must apply DB grants
Restart Policy:
• unless-stopped

8. workflow-engine (Business Process Management)

Purpose: Workflow execution (8 node types), step orchestration, timer events

Technology:
• Node.js 20+
• Redis PubSub (workflow:step:complete)
• Port 3003 (Internal API)

Architecture:NO database connection
• All data via Backend Internal API
• Event-Driven (Redis PubSub)

Environment Variables:
• REDIS_URL: redis://:PASSWORD@redis:6379 (REQUIRED!)
• REDIS_PASSWORD: For PubSub connection
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: For Backend API
• PORT: 3003 (Internal-API)
• SLA_CHECK_INTERVAL_MINUTES: 5 (Timer-Checker)
• RECOVERY_STUCK_THRESHOLD_MINUTES: 10 (Startup-Recovery)
• CIRCUIT_BREAKER_THRESHOLD: 5 (Error threshold)

Health Check:
• node fetch http://localhost:3003/health/live
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Kubernetes-Style Probes:
• /health/live (Liveness: Process running?)
• /health/ready (Readiness: Redis connected?)
• /health (Startup: Full check)

Depends On:
• redis (service_healthy) ← Required: the workflow engine needs Redis
• backend (service_healthy)

Restart Policy:
• unless-stopped

9. notification-worker (External Notifications)

Purpose: Notifications to external services (Webex, Teams, WebPush)

Technology:
• Node.js 20+
• BullMQ (Redis-Queue)
• WebPush (Browser-Notifications)
• Webex/Teams-SDKs
• Health-Port: 3006

Architecture:NO database connection
• All data via Backend Internal API
• User-Preferences via API
• Adapter-Configs via API

Environment Variables:
• REDIS_URL: redis://:PASSWORD@redis:6379
• REDIS_PASSWORD: Redis password
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: For Backend API
• HEALTH_PORT: 3006
• LOG_LEVEL: info (debug, info, warn, error)

Health Check:
• node dist/healthcheck.js
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Depends On:
• redis (service_healthy)
• backend (service_healthy)

Restart Policy:
• unless-stopped

10. report-generator (Reports & CSV Export)

Purpose: Report generation, CSV export, dashboard data
Technology:
• Node.js 20+
• BullMQ (Redis-Queue)
• Prisma ORM (Read-Only DB access)
• Port 3004

Architecture:READ-ONLY DB-User: helpdesk_readonly
• Cannot modify data (SELECT only)
• BullMQ for async report jobs
• Multi-instance capable (BullMQ-based)
Environment Variables:
• DATABASE_URL: postgresql://helpdesk_readonly:PASSWORD@db/helpdesk_db (Read-Only!)
• REDIS_URL: redis://:PASSWORD@redis:6379
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: For Backend API
• PORT: 3004
• COMPANY_NAME: Company name in report exports (independent of the company name in the Admin Center)
• CSV_DELIMITER: ";" (German), "," (international), "tab"
Health Check:
• node fetch http://localhost:3004/health/live
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Security Hardening:
• read_only: true, tmpfs: /tmp
• security_opt: no-new-privileges:true
• cap_drop: ALL
• No volumes (Prisma schema baked into image)
Depends On:
• db (service_healthy)
• redis (service_healthy)
• backend (service_healthy)

Restart Policy:
• unless-stopped

11. clamav (Virus Scanner Daemon)

Purpose: Virus scan daemon (ClamAV engine)

Technology:
• ClamAV 1.5.1 (Official Image)
• Freshclam (Auto-Update Virus-Definitions)
• Port 3310 (Internal, not exposed)

Environment Variables:
• FRESHCLAM_DAEMON: yes (Auto-Updates)
• CLAMD_DAEMON: yes (Daemon-Mode)
• FRESHCLAM_CHECKS: 24 (Updates every 60min)

Volumes:
• clamav_data:/var/lib/clamav (Virus-Definitions)
• uploads:/app/uploads:ro (Read-Only Upload-Access!)

Security Hardening:
• security_opt: no-new-privileges:true
• cap_drop: NET_RAW, SYS_ADMIN, MKNOD
• Read-Only Upload-Access (cannot modify files)

Resource Limits:
• Memory: 2GB Limit, 512MB Reservation
• CPU: 2.0 Cores

Health Check:
• clamdcheck.sh (Official ClamAV-Script)
• Interval: 60s, Timeout: 10s, Retries: 3
• Start-Period: 180s (3min for virus DB load)

Logging:
• max-size: 10MB, max-file: 3 (Rotation)

Restart Policy:
• unless-stopped

12. av-worker (Virus Scan Worker)

Purpose: Polls new uploads, sends to ClamAV, updates status via Backend API
Technology:
• Node.js 20+
• ClamAV-Client (TCP 3310)
• Health-Port: 3007

Isolation:
• No database access
• No access to uploaded files (passes only the file path to ClamAV)
• Communicates only via:
  - TCP with ClamAV (Port 3310)
  - HTTP with Backend (Internal-API)

Environment Variables:
• BACKEND_URL: http://backend:3000
• INTERNAL_API_KEY: For Backend API
• CLAMAV_HOST: clamav (DNS)
• CLAMAV_PORT: 3310
• HEALTH_PORT: 3007
• REDIS_URL: redis://:PASSWORD@redis:6379
• SCAN_POLL_CRON: */10 * * * * * (every 10 seconds)
• SCAN_BATCH_SIZE: 5 (Max parallel scans)
• SCAN_TIMEOUT_MS: 120000 (2 min per scan)

Hardening:read_only: true (Container-Filesystem read-only!)
• tmpfs: /app/tmp + /tmp (for temporary files)
• security_opt: no-new-privileges:true
• cap_drop: ALL (All Linux capabilities removed!)

Resource Limits:
• Memory: 1.5GB Limit, 256MB Reservation
• CPU: 1.0 Core
• NODE_OPTIONS: --max-old-space-size=256

Health Check:
• node dist/healthcheck.js
• Interval: 15s, Timeout: 5s, Retries: 3
• Start-Period: 30s

Depends On:
• clamav (service_healthy) ← Waits for ClamAV ready
• backend (service_healthy)

Restart Policy:
• unless-stopped

Networking

Project Network:

• Docker Compose automatically creates a dedicated bridge network for the stack
• All containers can reach each other via DNS
• DNS names = service names (e.g. "backend", "db", "redis")
Service Discovery (Examples):

Traefik → Frontend:  http://frontend:80
Traefik → Backend:   http://backend:3000
Backend → DB:        postgresql://helpdesk_user@db:5432/helpdesk_db
Backend → Redis:     redis://:PASSWORD@redis:6379
Worker → Backend:    http://backend:3000
AV-Worker → ClamAV:  tcp://clamav:3310
Report → DB:         postgresql://helpdesk_readonly@db:5432/helpdesk_db

Exposed Ports (Host → Container):

80:80       → Traefik (HTTP → HTTPS Redirect)
443:443     → Traefik (HTTPS)
3000:3000   → Backend API

Internal-Only Ports (not exposed):

80          → Frontend (only via Traefik)
5432        → PostgreSQL (internal only)
6379        → Redis (internal only)
3001        → Job-Worker (Health)
3003        → Workflow-Engine
3004        → Report-Generator
3005        → Email-Worker (Health)
3006        → Notification-Worker (Health)
3007        → AV-Worker (Health)
3310        → ClamAV (only for av-worker)

Depends-On & Startup Order

Startup Order:

1. db + redis (start in parallel, no dependencies)
   │
   ├─ db: PostgreSQL starts
   │  └─ Init-Scripts run (01-create-users.sh)
   │
   └─ redis: Redis starts with AOF persistence + password
2. backend (waits for db + redis)
   │
   ├─ Connects to db + redis
   ├─ Prisma-Migrations run (automatic)
   ├─ DB-Grants for restricted users
   ├─ Seed-Data (if SEED_DATABASE=true)
   └─ Health-Check: /api/health/live → HEALTHY

3. Worker + Report-Generator (wait for backend.service_healthy)
   │
   ├─ email-worker: Connects to redis, Backend API
   ├─ job-worker: Connects to db (restricted), redis, Backend API
   ├─ workflow-engine: Connects to redis, Backend API
   ├─ notification-worker: Connects to redis, Backend API
   └─ report-generator: Connects to db (readonly), redis, Backend API
4. clamav (starts in parallel)
   │
   ├─ Loads virus definitions (can take 2-3 minutes)
   └─ Health-Check: clamdcheck.sh → HEALTHY

5. av-worker (waits for clamav.service_healthy + backend.service_healthy)
   │
   ├─ Connects to ClamAV (TCP 3310)
   ├─ Connects to Backend API
   └─ Starts polling (every 10s)
6. frontend (waits for backend.service_healthy)
   │
   ├─ NGINX starts
   └─ Health-Check: wget http://localhost:80/health → HEALTHY

7. traefik (waits for backend + frontend healthy)
   │
   ├─ Loads configuration (traefik.yml + dynamic.yml)
   └─ Health-Check: traefik healthcheck --ping → HEALTHY

Critical Path:

db → backend → [workers, frontend] → traefik
                                   → av-worker

Total Startup Time:
• Without ClamAV: ~45-60 seconds
• With ClamAV: ~3-4 minutes (virus DB load)

Health Checks

Service Test Interval Start-Period
traefiktraefik healthcheck --ping15s10s
frontendwget -qO- http://localhost:80/health15s10s
backendnode fetch /api/health/live15s300s
dbpg_isready -U helpdesk_user -d helpdesk_db15s30s
redisredis-cli -a PASSWORD ping | grep PONG15s10s
email-workernode dist/healthcheck.js15s30s
job-workernode fetch /health/live (port 3001)15s30s
workflow-enginenode fetch /health/live (port 3003)15s30s
notification-workernode dist/healthcheck.js15s30s
report-generatornode fetch /health/live (port 3004)15s30s
clamavclamdcheck.sh60s180s
av-workernode dist/healthcheck.js15s30s

Volumes & Persistence

Named Volumes

Volume Purpose Size (estimated)
postgres_dataPostgreSQL data (CRITICAL!)10-100GB (depends on data)
redis_dataRedis AOF files (cache/queue)100MB-1GB
uploadsUser uploads (attachments)1GB-1TB (depends on usage)
quarantineInfected files (isolated)< 100MB (rare)
clamav_dataVirus definitions (daily updates)500MB-1GB
sourcemapsFrontend JS source maps, the five newest releases (backend reads read-only for error tracking)100-500MB (5 releases)

Volume Backup Strategy

# CRITICAL: postgres_data (daily!)
docker run --rm \
  -v eviworx_postgres_data:/data \
  -v /backup:/backup \
  alpine tar czf /backup/postgres-$(date +%Y%m%d).tar.gz /data

# IMPORTANT: uploads (weekly)
docker run --rm \
  -v eviworx_uploads:/data \
  -v /backup:/backup \
  alpine tar czf /backup/uploads-$(date +%Y%m%d).tar.gz /data

# Optional: redis_data, clamav_data (can be rebuilt)
Note: See Installation documentation for full backup strategy with pg_dump (better option than volume backup).

Commands

Startup

# Pull images and start all containers
docker compose pull
docker compose up -d

# Only specific services
docker compose up -d backend db redis

Shutdown

# Stop all containers (volumes remain)
docker compose down

# With volume deletion (CAUTION!)
docker compose down -v

# Only stop (do not remove)
docker compose stop

Troubleshooting

# Check logs
docker compose logs backend
docker compose logs -f backend  # Follow mode

# Health status
docker compose ps
docker inspect eviworx-backend | grep -A 5 Health

# Restart container
docker compose restart backend

# Into container shell
docker exec -it eviworx-backend sh

Production Checklist

  • All secrets changed (JWT_SECRET, SHARE_SECRET, INTERNAL_API_KEY, DB passwords, REDIS_PASSWORD, LICENSE_ENCRYPTION_KEY, TWO_FACTOR_ENCRYPTION_KEY)
  • SEED_DATABASE=false set (after first start)
  • Resource limits set for all services
  • Traefik configured (traefik.yml + dynamic.yml + SSL certificates)
  • SSL certificates installed
  • Backup jobs configured (postgres_data, uploads)
  • Health checks configured
  • Log aggregation (ELK, Loki)
  • Firewall rules (only 80/443 from outside)
  • Cloudflare Turnstile configured (bot protection)