Eviworx
Docs

Container Architecture

Eviworx runs in 12 containers under Docker Compose. Each container has a clearly defined job, is hardened (read-only filesystem, dropped capabilities, non-root users), has a health check and talks to the others over the Docker network. Traefik is the API gateway and terminates TLS.

🐳
Features
✓ 12 production containers (including 5 workers)
✓ API gateway with TLS termination (Traefik v3.7)
✓ Hardening (read-only, cap_drop, non-root)
✓ Health checks for all containers (15–60 s)
✓ Memory and CPU limits (all 12 containers)
✓ Least-privilege DB (3 PostgreSQL users)
✓ Worker authentication (INTERNAL_API_KEY)
✓ 6 named volumes (e.g. postgres_data, uploads)
✓ Horizontally scalable workers (--scale)
✓ Isolated virus scan (no upload access)

Container Overview

Container Port Technology Responsibility
traefik80, 443, 8082Traefik v3.7API gateway, TLS termination, routing, rate limiting
frontend80 (internal)Nginx + React 19Web UI, static SPA files
backend3000Node.js 24 + ExpressREST API, business logic
postgres5432 (internal)PostgreSQL 17Primary database
redis6379 (internal)Redis 8Cache, queues, pub/sub
clamav3310ClamAV 1.5.1Virus scan daemon
email-worker3005Node.js 20Email processing (IMAP/SMTP/Graph API)
notification-worker3006Node.js 20Multi-channel notifications
job-worker3001Node.js 24CronJobs, SLA monitor
av-worker3007Node.js 24Virus scan coordination
workflow-engine3003Node.js 20Workflow execution
report-generator3004Node.js 24Report generation, CSV/PDF export

Container Details

1. Traefik (API-Gateway & TLS-Termination)

TRAEFIK CONTAINER

Image: traefik:v3.7.7

Ports:
• 80:80 → HTTP (redirect to HTTPS)
• 443:443 → HTTPS (TLS 1.2+)
• 8082 (internal) → Ping endpoint (health check)

Responsibilities:
• API-Gateway: Single entry point for all external traffic
• TLS-Termination: TLS 1.2/1.3 with strong cipher suites
• Routing:
  /api/* → http://backend:3000
  /socket.io/* → http://backend:3000 (WebSocket upgrade)
  / → http://frontend:80 (SPA fallback)
• Rate-Limiting: 100 req/s average, burst 200
• Security-Headers: HSTS, CSP, X-Frame-Options, X-Content-Type-Options
• Internal interfaces between the services not reachable from outside (middleware)

Configuration:
• traefik/traefik.yml → Static configuration (entrypoints, providers, ping, forwardedHeaders.trustedIPs)
• traefik/dynamic.yml → Dynamic configuration (routers, middlewares, TLS)

External Reverse Proxy:
• When behind external proxy: configure forwardedHeaders.trustedIPs in traefik.yml• Additionally set TRUSTED_PROXIES in .env (see Installation)
Volumes:
• ./traefik/traefik.yml:/etc/traefik/traefik.yml:ro
• ./traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro
• ./certs/cert.pem:/etc/traefik/ssl/cert.pem:ro
• ./certs/cert.key:/etc/traefik/ssl/cert.key:ro

Health Check:
test: ["CMD", "traefik", "healthcheck", "--ping", "--ping.entrypoint=ping"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s

Dependencies:
• backend (service_healthy)
• frontend (service_healthy)

Security:
• security_opt: no-new-privileges:true
• TLS 1.2+ only (no SSLv3, TLS 1.0, TLS 1.1)
• Strong ciphers: ECDHE-*, DHE-RSA-AES*, ChaCha20
• HSTS: max-age=31536000; includeSubDomains
• CSP: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
• X-Frame-Options: SAMEORIGIN
• X-Content-Type-Options: nosniff

Logging:
• JSON format access logs
• Structured request/response logging

Restart: unless-stopped

2. Frontend (Nginx + React SPA)

FRONTEND CONTAINER

Image: nginx:stable-alpine (production)
Build: node:24-alpine (build stage)

Stack:
• React 19.2.0
• Vite 7.2.7 (build tool)
• TanStack Query v5 (data fetching)
• Socket.io Client v4.8.1 (real-time)
• Tailwind CSS v4 (styling)
• Radix UI (components)

Ports:
• 80 (internal only, exposed to Traefik)

Responsibilities:
• Serves static SPA files via Nginx
• SPA fallback: app routes → index.html
• Note: Reverse proxy, TLS, and security headers handled by Traefik

Delivery & Caching:
• Build files under /_app/ (hashed names): public, max-age=31536000, immutable
• Missing file under /_app/ → 404 (no HTML fallback)
• /sw.js, /manifest.webmanifest → no-cache
• index.html and app routes → no-cache, must-revalidate
• Source maps (.map) are not served over HTTP
• Upstream proxies: pass /_app/* and /sw.js through unchanged and do not add caching

Health Check:
test: ["CMD", "wget", "-qO-", "http://localhost:80/health"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s

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

Security Hardening:
• read_only: true, tmpfs: /var/cache/nginx, /var/run, /tmp
• security_opt: no-new-privileges:true
• cap_drop: NET_RAW, SYS_ADMIN, MKNOD

Environment:
• TRUSTED_PROXIES (for correct client IP logging behind external proxy)

Dependencies:
Depends on: backend (service_healthy)

Restart: unless-stopped

3. Backend (Node.js Express API)

BACKEND CONTAINER

Image: node:24-slim

Stack:
• Express v5.2.1 (REST API)
• Prisma v7.5.0 (ORM)
• BullMQ v5.71.0 (job queues)
• Socket.io v4.8.3 (real-time updates)
• JWT (jsonwebtoken v9.0.3)
• PBKDF2-SHA512 (FIPS 140-2 compatible)
• Pino v10.3.1 (structured logging)
• Sharp v0.34.5 (image processing)
• Web-Push v3.6.7 (push notifications)

Ports:
• 3000:3000 → REST API

Volumes:
• uploads:/app/uploads (user-uploaded files)
• quarantine:/app/quarantine (infected files)
• sourcemaps:/opt/sourcemaps:ro (frontend source maps, read-only)

Dependencies:
• PostgreSQL (db:5432)
• Redis (redis:6379, password-authenticated)

Health Check:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/api/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 300s  # 5 min (migrations + seed)

Environment:
DATABASE_URL=postgresql://helpdesk_user:***@db:5432/helpdesk_db
JWT_SECRET=*** (CHANGE IN PRODUCTION!)
JWT_SECRET_OLD=*** (for secret rotation, optional)
SHARE_SECRET=*** (REQUIRED — signs public share links, backend won't boot without it!)
INTERNAL_API_KEY=*** (for worker auth)
LICENSE_ENCRYPTION_KEY=*** (AES-256-GCM for license keys)
TWO_FACTOR_ENCRYPTION_KEY=*** (AES-256-GCM for 2FA secrets)
ADMIN_INITIAL_PASSWORD=*** (only used on first run with empty DB)
VAPID_PUBLIC_KEY/PRIVATE_KEY=*** (Web Push)
MAX_FILE_SIZE=104857600 (100MB)
REDIS_URL=redis://:PASSWORD@redis:6379
TURNSTILE_SITE_KEY=*** (Cloudflare Turnstile CAPTCHA)
TURNSTILE_SECRET_KEY=*** (Cloudflare Turnstile Secret)
ENABLE_FIPS=false (FIPS crypto mode)
PBKDF2_ITERATIONS=210000 (PBKDF2-SHA512 iterations)
SESSION_MAX_HOURS=12
ACCESS_TOKEN_EXPIRY_MINUTES=60
REFRESH_TOKEN_EXPIRY_MINUTES=100
IDLE_TIMEOUT_MINUTES=30
COOKIE_SECURE=true (otherwise NODE_ENV-dependent)
GLOBAL_RATE_LIMIT_MAX=2000, AUTH_FAIL_PER_PAIR_MAX=5, REFRESH_FAIL_PER_IP_MAX=60, …
   (13 rate limits, all ENV-configurable — see environment page)

Password Hashing:
• Default: PBKDF2-SHA512 (FIPS 140-2 compatible)
• FIPS mode: ENABLE_FIPS=true (optional FIPS crypto module)

Init Sequence:
1. Check database state (new or existing)
2. Apply migrations or create the schema
3. Apply permissions for the restricted DB users
4. Seed the database if SEED_DATABASE=true
5. Start the API server

Restart: unless-stopped

4. PostgreSQL (Database)

POSTGRES CONTAINER

Image: eviworx/db (based on PostgreSQL 17.5 Alpine, init scripts baked in)

Ports:
• 5432 (internal only, not exposed externally)

Volumes:
• postgres_data:/var/lib/postgresql/data (persistence)

Init Scripts (alphabetical):
• 01-create-users.sh → Create restricted DB users

3 Database Users (Least Privilege):

1. helpdesk_user (Main)
   • Full access to all tables
   • Used by Backend for migrations
   • Password: POSTGRES_PASSWORD (supersecretpassword - CHANGE!)

2. helpdesk_jobworker (Restricted)
   • SELECT, INSERT, UPDATE, DELETE on:
     - CronJob
     - JobExecution
     - WorkerInstance
   • USAGE on all sequences (auto-increment)
   • Password: JOBWORKER_DB_PASSWORD env

3. helpdesk_readonly (Analytics)
   • SELECT only on all tables
   • Used for reporting (report-generator)
   • Password: READONLY_DB_PASSWORD env

Health Check:
test: ["CMD-SHELL", "pg_isready -U helpdesk_user -d helpdesk_db"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Security:
• Principle of Least Privilege (3 users)
• Init scripts read-only mount
• Named volume isolation
• Port not exposed externally (expose only)

Restart: unless-stopped

5. Redis (Cache & Queue System)

REDIS CONTAINER

Image: redis:8.6-alpine

Ports:
• 6379 (internal only, not exposed externally)

Volumes:
• redis_data:/data (AOF persistence)

Persistence:
• Append-Only File (AOF) enabled
• Command: redis-server --appendonly yes --requirepass PASSWORD

Use Cases:
• Caching (Permissions, Business Hours, Holidays)
• BullMQ Queues (Email, Notifications, Jobs)
• Rate-Limiting (API keys only; the HTTP limiters count in backend memory)
• Session Storage
• Pub/Sub (Domain-Events)
• Distributed Locks (Multi-Instance Coordination)

Health Check:
test: ["CMD-SHELL", "redis-cli -a PASSWORD --no-auth-warning ping | grep PONG"]
interval: 15s
timeout: 5s
retries: 3
start_period: 10s

Security:
• Password authentication (--requirepass)
• AOF for data durability
• Port not exposed externally (expose only)

Restart: unless-stopped

6. ClamAV (Antivirus Daemon)

CLAMAV CONTAINER

Image: clamav/clamav:1.5.1

Internal Port: 3310 (clamd TCP socket)

Volumes:
• clamav_data:/var/lib/clamav (virus definitions)
• uploads:/app/uploads:ro (read-only file scanning)

Resource Limits:
limits:
  memory: 2G
  cpus: '2.0'
reservations:
  memory: 512M

Security Hardening:
security_opt:
  - no-new-privileges:true
cap_drop:
  - NET_RAW
  - SYS_ADMIN
  - MKNOD

Health Check:
test: ["CMD", "clamdcheck.sh"]
interval: 60s
timeout: 10s
retries: 3
start_period: 180s  # 3 minutes (load virus defs)

Auto-Update:
• Freshclam daemon enabled
• Checks 24x/day (FRESHCLAM_CHECKS=24 → hourly)
• Downloads latest virus definitions

Logging:
driver: json-file
max-size: 10m
max-file: 3

Restart: unless-stopped

7. Email-Worker (E-Mail Processing)

EMAIL-WORKER CONTAINER

Image: node:20-slim

Ports:
• 3005 (internal, health endpoint)

Stack:
• nodemailer v6.9.7 (SMTP client)
• imap v0.8.19 (IMAP client)
• Microsoft Graph API (Microsoft 365 mailboxes)
• mailparser v3.6.5 (E-Mail parsing)
• BullMQ v5.65.0 (queue consumption)
• Express v4.21.0 (health endpoint)
• Pino v10.3.1 (structured logging)

Responsibilities:
• IMAP Polling (check for new emails)
• Microsoft Graph API (Microsoft 365 support)
• Individual mailbox support
• E-Mail-to-Ticket conversion
• Thread-Matching (RFC 822)
• Bounce-Detection
• SMTP Sending (outbound replies)
• Match replies to the existing ticket
• Dismiss emails
• Email signatures
• Verify the mail server's TLS certificate
Dependencies:
• Redis (6379, password-authenticated) - BullMQ queue
• Backend API (3000) - config + status updates

Health Check:
test: ["CMD", "node", "dist/healthcheck.js"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Environment:
REDIS_URL=redis://:PASSWORD@redis:6379
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
HEALTH_PORT=3005
EMAIL_INBOUND_MAX_SIZE_MB=25
EMAIL_ACCENT_COLOR=#3b8f93
EMAIL_APP_NAME=*** (branding)
EMAIL_APP_URL=*** (branding)
EMAIL_FOOTER_TEXT=*** (branding)
EMAIL_LAYOUT_ENABLED=true
EMAIL_INBOUND_RATE_LIMIT_PER_MINUTE=60
EMAIL_INBOUND_RATE_LIMIT_PER_SENDER_PER_HOUR=30

Security:
• Non-root user: nodejs (UID 1001)
• No database access (all data via the internal backend API)
• Dumb-init for signal handling

Restart: unless-stopped

8. Job-Worker (Background Jobs & CronJobs)

JOB-WORKER CONTAINER

Image: node:24-slim

Ports:
• 3001 (internal, health endpoint)

Stack:
• Express v5.1.0 (health endpoint)
• Prisma v7.5.0 (restricted DB access)
• BullMQ v5.65.0 (task queue)
• Bottleneck v2.19.5 (rate limiting)
• Opossum v9.0.0 (circuit breaker)
• Pino v10.3.1 (structured logging)

Responsibilities:
• CronJob execution (26+ Action-Types)
• SLA monitoring (every 5 minutes)
• Background tasks scheduling
• Worker heartbeat tracking
• Multi-instance coordination

Dependencies:
• PostgreSQL (5432) - RESTRICTED user: helpdesk_jobworker
  - Tables: CronJob, JobExecution, WorkerInstance
• Redis (6379, password-authenticated) - distributed locking
• Backend API (3000) - domain data

Volumes:
• None (Prisma schema baked into image)
Health Check:
test: ["CMD", "node", "-e", "fetch('http://localhost:3001/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Environment:
DATABASE_URL=postgresql://helpdesk_jobworker:***@db:5432/helpdesk_db
REDIS_URL=redis://:PASSWORD@redis:6379
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
METRICS_PORT=3001

Security:
• Restricted DB user (no access to tickets/users)
• Non-root user: jobworker
• Circuit breaker for API failures

Multi-Instance:
• Auto-generated INSTANCE_ID
• Distributed locks via Redis
• Worker heartbeat tracking

Restart: unless-stopped

9. Notification-Worker (Multi-Channel Dispatch)

NOTIFICATION-WORKER CONTAINER

Image: node:20-alpine

Ports:
• 3006 (internal, health endpoint)

Stack:
• BullMQ v5.65.0 (queue consumption)
• Express v4.21.0 (health endpoint)
• Pino v10.3.1 (structured logging)
• Axios (HTTP client for Teams/Webex)

Responsibilities:
• Process notification:send queue
• Multi-channel dispatch:
  - EMAIL (via Email-Worker)
  - TEAMS (Microsoft Teams Bot Framework)
  - WEBEX (Cisco Webex Bot API)
  - IN_APP/PUSH (via Backend API)
• Attachment support for notifications
• Quiet Hours (notification suppression)

Dependencies:
• Redis (6379, password-authenticated) - BullMQ queue
• Backend API (3000) - notification config

Health Check:
test: ["CMD", "node", "dist/healthcheck.js"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Environment:
REDIS_URL=redis://:PASSWORD@redis:6379
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
LOG_LEVEL=info
HEALTH_PORT=3006

Security:
• Non-root user: nodejs (UID 1001)
• No database access (all data via the internal backend API)
• Dumb-init for signal handling

Restart: unless-stopped

10. AV-Worker (Virus-Scan Worker)

AV-WORKER CONTAINER

Image: node:24-alpine

Ports:
• 3007 (internal, health endpoint)

Stack:
• Axios v1.6.2 (HTTP client)
• Express v5.1.0 (health endpoint)
• Node-cron v4.2.1 (polling scheduler)
• Pino v10.3.1 (structured logging)

Responsibilities:
• Poll for unscanned attachments (every 10 seconds)
• Initiate ClamAV scans via TCP protocol
• Update Backend with scan results
• Report infected files to the backend (the backend moves them to quarantine)

Dependencies:
• ClamAV (3310) - virus scanning
• Backend API (3000) - scan status updates
• Redis (6379, password-authenticated)

Resource Limits:
limits:
  memory: 1536M
  cpus: '1.0'
reservations:
  memory: 256M

HARDENING:
read_only: true  # ← Filesystem completely read-only!

tmpfs:
  - /app/tmp:size=64M,mode=1777
  - /tmp:size=64M,mode=1777

security_opt:
  - no-new-privileges:true

cap_drop:
  - ALL  # ← All capabilities dropped!

Important:
• AV-Worker has NO access to the uploads volume!
• Communicates only via:
  1. ClamAV TCP API (sends path, not content)
  2. Backend HTTP API (status updates)

Health Check:
test: ["CMD", "node", "dist/healthcheck.js"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Environment:
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
CLAMAV_HOST=clamav
CLAMAV_PORT=3310
SCAN_POLL_CRON=*/10 * * * * *  # Every 10 seconds (6-field cron)
SCAN_BATCH_SIZE=5
SCAN_TIMEOUT_MS=120000  # 2 minutes per scan
HEALTH_PORT=3007
REDIS_URL=redis://:PASSWORD@redis:6379

Security:
• Non-root user: avworker (UID 1001)
• Read-only filesystem
• No capabilities
• Tmpfs for temporary files only

Logging:
driver: json-file
max-size: 10m
max-file: 3

Restart: unless-stopped

11. Workflow-Engine (Business Process Automation)

WORKFLOW-ENGINE CONTAINER

Image: node:20-alpine

Ports:
• 3003 (internal, API + health probes)

Stack:
• Express v4.18.2 (internal API)
• redis (node-redis) v5.9.0 (Redis Pub/Sub task intake)
• Node-cron v3.0.3 (scheduled tasks)
• Pino v10.3.1 (structured logging)

Responsibilities:
• Workflow state machine execution
• 8 Node-Typen (Manual Task, Approval, Automated Action, etc.)
• 7 Actions (Send Email, Webhook, Create Ticket, etc.)
• SLA tracking integration
• Circuit breaker for API resilience

Dependencies:
• Redis (6379, password-authenticated) - state management, Pub/Sub triggers
• Backend API (3000) - workflow config, domain data

Health Check:
test: ["CMD", "node", "-e", "fetch('http://localhost:3003/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Health Endpoints:
• /health/live → Liveness probe (process running)
• /health/ready → Readiness probe (Redis connected)
• /health → Full health check (Redis)

Environment:
REDIS_URL=redis://:PASSWORD@redis:6379
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
PORT=3003
SLA_CHECK_INTERVAL_MINUTES=5
CIRCUIT_BREAKER_THRESHOLD=5
CIRCUIT_BREAKER_RESET_MS=30000

Security:
• Non-root user: nodejs (UID 1001)
• Circuit breaker protects against Backend failures
• Dumb-init for signal handling

Restart: unless-stopped

12. Report-Generator (Custom Reports & Export)

REPORT-GENERATOR CONTAINER

Image: node:24-alpine

Ports:
• 3004 (internal, API + health probes)

Stack:
• Express v5.1.0 (REST API)
• Prisma v7.5.0 (ORM, read-only DB access)
• BullMQ v5.65.0 (scheduled reports queue)
• Pino v10.3.1 (structured logging)

Responsibilities:
• Custom report query execution
• CSV export generation
• PDF export generation
• Scheduled reports via BullMQ

Database Access:
• READ-ONLY access (helpdesk_readonly user)
• No write permissions to any table

Dependencies:
• PostgreSQL (5432) - READ-ONLY user: helpdesk_readonly
• Redis (6379, password-authenticated) - BullMQ queue
• Backend API (3000) - configuration, auth

Volumes:
• None (Prisma schema baked into image)
Health Check:
test: ["CMD", "node", "-e", "fetch('http://localhost:3004/health/live').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
retries: 3
start_period: 30s

Health Endpoints:
• /health → Full health check
• /health/live → Liveness probe
• /health/ready → Readiness probe

Environment:
DATABASE_URL=postgresql://helpdesk_readonly:***@db:5432/helpdesk_db
REDIS_URL=redis://:PASSWORD@redis:6379
BACKEND_URL=http://backend:3000
INTERNAL_API_KEY=***
PORT=3004
COMPANY_NAME=*** (for report headers)
CSV_DELIMITER=; (configurable: ";", ",", "tab")

Security:
• Non-root user: nodejs (UID 1001)
• Dumb-init for signal handling
• Read-only database access (helpdesk_readonly)

Restart: unless-stopped

Inter-Container Communication

COMMUNICATION FLOW

USER REQUEST FLOW:

User Browser
  ↓ HTTPS (443)
Traefik (API-Gateway, TLS-Termination)
  ├─ /api/* → Backend (3000)
  ├─ /socket.io/* → Backend (3000, WebSocket upgrade)
  └─ /* → Frontend (80, SPA)

Frontend (nginx:80) → static SPA files only

Backend (3000)
  ├─ TCP 5432 → PostgreSQL (helpdesk_user)
  ├─ TCP 6379 → Redis (password-authenticated, cache, queues)
  └─ Publish domain events → Redis Pub/Sub

WORKER COMMUNICATION:

Redis (6379, password-authenticated)
  ├─ BullMQ Queues:
  │   ├─ email:send → Email-Worker
  │   ├─ notification:send → Notification-Worker
  │   ├─ job:execute → Job-Worker
  │   ├─ workflow:execute → Workflow-Engine
  │   └─ report:generate → Report-Generator
  │
  └─ Pub/Sub Events:
      ├─ domain:events → Workers subscribe
      └─ notification:result → Backend

All Workers → Backend API (3000)
  ├─ GET config/domain data
  ├─ POST status updates
  └─ Auth: Bearer INTERNAL_API_KEY

Report-Generator → PostgreSQL (5432)
  └─ READ-ONLY queries (helpdesk_readonly user)

VIRUS-SCAN FLOW:

AV-Worker (polling every 10 seconds)
  ↓ HTTP
Backend API (fetch pending scans)
  ↓ Returns: [{id, path, ...}]
AV-Worker
  ↓ TCP 3310 (clamd protocol)
ClamAV SCAN /app/uploads/file.pdf
  ↓ Returns: CLEAN / INFECTED
AV-Worker
  ↓ HTTP
Backend API (report scan result)
  ↓ Update scan status

EMAIL FLOW:

Email-Worker (IMAP polling / Microsoft Graph API)
  ↓ IMAP protocol / Graph API
External IMAP server / Microsoft 365 (support@company.com)
  ↓ Parse email
Email-Worker
  ↓ HTTP
Backend API (process inbound email)
  ↓ Create ticket, link attachments
Backend publishes domain event
  ↓ Redis Pub/Sub
Notification-Worker
  ↓ Queue notification:send
Process notification
  ↓ SMTP (via Email-Worker) or Teams Bot Framework / Webex API
Send notification

Security Architecture

Defense in Depth

Layer Measure Container
Transport TLS 1.2/1.3, HSTS, strong ciphers, rate limiting (100 req/s) Traefik
Gateway Security headers (CSP, X-Frame-Options), internal interfaces blocked externally Traefik
Application JWT auth, RBAC (28+ modules), PBKDF2-SHA512 (FIPS 140-2 compatible), Turnstile CAPTCHA Backend
Worker Auth INTERNAL_API_KEY (Bearer Token) All workers
Database 3 DB users with minimal rights (least privilege), port not exposed externally PostgreSQL
Cache/Queue Password authentication (--requirepass), port not exposed externally Redis
Filesystem Read-only filesystem, tmpfs, cap_drop ALL All workers, report generator
Virus Scan ClamAV with automatic signature updates (Freshclam) ClamAV
Isolation Quarantine volume for infected files Backend
Reporting Read-only DB user Report-Generator

Container Hardening Comparison

Container Read-Only Non-Root Cap-Drop no-new-priv Resources Health Check
AV-Worker✅ ALL1.5G / 1C✅ Custom
Email-Worker✅ ALL512M / 1C✅ Custom
Job-Worker✅ ALL1G / 1C✅ HTTP
Notification-Worker✅ ALL512M / 1C✅ Custom
Workflow-Engine✅ ALL512M / 1C✅ HTTP
Report-Generator✅ ALL1.5G / 1C✅ HTTP
Frontend✅ 3 caps256M / 0.5C✅ HTTP
TraefikPartial (ro)256M / 1C✅ Ping
ClamAV✅ 3 caps2G / 2C✅ Script
Backend✅ 3 caps2G / 2C✅ HTTP
PostgreSQL✅ 2 caps2G / 2C✅ pg_isready
Redis✅ 2 caps1G / 1C✅ redis-cli

🔒 Isolated virus scan: The av-worker runs with a read-only filesystem and no Linux capabilities and has no access to uploaded files. It only communicates with ClamAV and the backend API.

Named Volumes

Volume Mounted by Purpose
postgres_data postgres PostgreSQL database files
redis_data redis Redis AOF persistence
uploads backend (rw), clamav (ro) User-uploaded attachments
quarantine backend Infected files (isolation)
clamav_data clamav Virus definitions (Freshclam)
sourcemaps frontend (rw), backend (ro) Frontend JS source maps for error tracking, the five newest releases (backend reads read-only)

Startup Sequence & Dependencies

CONTAINER STARTUP SEQUENCE (12 Containers):

1. PostgreSQL (db)
   • Starts first (no dependencies)
   • Loads init scripts from /docker-entrypoint-initdb.d
   • Creates restricted users (jobworker, readonly)
   • Health: pg_isready

2. Redis (redis)
   • Starts in parallel with PostgreSQL
   • Loads AOF file (if present)
   • Password authentication active (--requirepass)
   • Health: redis-cli ping

3. Backend (backend)
   • Depends on: db, redis (service_started)
   • Waits for DB connection
   • Runs:
     ├─ Check database state (new or existing)
     ├─ Apply migrations or create the schema
     ├─ Apply permissions for the restricted DB users
     └─ Seed database (if SEED_DATABASE=true)
   • Starts Express server

4. Frontend (frontend)
   • Depends on: backend (service_healthy)
   • Waits until backend health check OK
   • Nginx starts (static SPA files)

5. Traefik (traefik)
   • Depends on: backend (service_healthy), frontend (service_healthy)
   • Waits until backend + frontend healthy
   • Starts API gateway, TLS termination, routing

6. ClamAV (clamav)
   • Starts in parallel (no dependencies)
   • Loads virus definitions (180s startup period)
   • Freshclam updates hourly (FRESHCLAM_CHECKS=24)

7. Email-Worker (email-worker)
   • Depends on: backend (service_healthy), redis (service_healthy)
   • Waits for backend + Redis
   • Starts IMAP/Graph API polling + queue consumption

8. Job-Worker (job-worker)
   • Depends on: backend (service_healthy), redis (service_healthy), db (service_healthy)
   • Waits for all dependencies
   • Registers worker instance
   • Starts heartbeat + CronJob scheduling

9. Notification-Worker (notification-worker)
   • Depends on: backend (service_healthy), redis (service_healthy)
   • Starts notification:send queue consumption

10. AV-Worker (av-worker)
   • Depends on: backend (service_healthy), clamav (service_healthy)
   • Waits for ClamAV ready (180s!)
   • Starts scan polling (every 10 seconds)

11. Workflow-Engine (workflow-engine)
   • Depends on: backend (service_healthy), redis (service_healthy)
   • Starts workflow execution engine

12. Report-Generator (report-generator)
   • Depends on: db (service_healthy), redis (service_healthy), backend (service_healthy)
   • READ-ONLY DB access (helpdesk_readonly user)
   • Executes queued report jobs (the schedule is paced by the job-worker)

TOTAL STARTUP TIME: ~3-5 Minuten
  • PostgreSQL: ~10s
  • Backend (with migrations): ~30-60s
  • ClamAV (virus defs): ~180s
  • Workers: ~10-15s after backend ready
  • Traefik: ~5s after backend + frontend ready

Resource Planning

Minimum Requirements

Hardware recommendations for development, production and high load are listed under Scaling & High Availability → Resource Planning.

Container-Specific Resource Limits

Container RAM Limit CPU Limit Reservation
traefik256MB1.064MB
frontend256MB0.532MB
backend2GB2.0512MB
db (PostgreSQL)2GB2.0256MB
redis1GB1.0128MB
email-worker512MB1.0128MB
job-worker1GB1.0128MB
workflow-engine512MB1.0128MB
notification-worker512MB1.0128MB
ClamAV2GB2.0512MB
AV-Worker1.5GB1.0256MB
report-generator1.5GB1.0256MB

💡 ClamAV Memory: Virus definitions require ~200MB+ RAM. 2GB limit is necessary for large signature databases.

Monitoring & Observability

Health Check Summary

Service Type Endpoint Interval Startup
TraefikPingtraefik healthcheck --ping15s10s
FrontendHTTPwget localhost:80/health15s10s
BackendHTTPlocalhost:3000/api/health/live15s300s
PostgreSQLCMD-SHELLpg_isready -U helpdesk_user -d helpdesk_db15s30s
RedisCMD-SHELLredis-cli -a PASSWORD ping | grep PONG15s10s
ClamAVScriptclamdcheck.sh60s180s
Email-WorkerCustomnode dist/healthcheck.js15s30s
Job-WorkerHTTPlocalhost:3001/health/live15s30s
Notification-WorkerCustomnode dist/healthcheck.js15s30s
AV-WorkerCustomnode dist/healthcheck.js15s30s
Workflow-EngineHTTPlocalhost:3003/health/live15s30s
Report-GeneratorHTTPlocalhost:3004/health/live15s30s

Health Endpoints

# Job-Worker Health Endpoints
GET http://localhost:3001/health/live   # Liveness
GET http://localhost:3001/health        # Full health

# Workflow-Engine Health Endpoints
GET http://localhost:3003/health/live   # Liveness
GET http://localhost:3003/health/ready  # Readiness
GET http://localhost:3003/health        # Full health

# Report-Generator Health Endpoints
GET http://localhost:3004/health/live   # Liveness
GET http://localhost:3004/health/ready  # Readiness
GET http://localhost:3004/health        # Full health

Structured Logging (JSON)

All Eviworx-owned containers log in structured JSON format via Pino v10. Logs include distributed tracing fields (traceId, spanId, correlationId) and are directly compatible with Elasticsearch/ELK, Loki, Datadog and other JSON log aggregators.

Container Log Format Rotation Tracing Fields
BackendJSON (Pino)-traceId, spanId, correlationId, requestId, sourceService
Job-WorkerJSON (Pino)-traceId, spanId, correlationId
Email-WorkerJSON (Pino)-traceId, spanId, correlationId
Notification-WorkerJSON (Pino)-traceId, spanId, correlationId
Workflow-EngineJSON (Pino)-traceId, spanId, correlationId
AV-WorkerJSON (Pino)10MB x 3traceId, spanId, correlationId
Report-GeneratorJSON (Pino)-traceId, spanId, correlationId
TraefikJSON (Access-Log)-X-Request-ID, User-Agent
ClamAVNative format10MB x 3-
PostgreSQLNative format--
RedisNative format--
// Example: Backend log entry with distributed tracing{
  "level": 30,
  "time": 1773740483502,
  "service": "backend",
  "traceId": "c5d1c321ad743f0abd05d55967adf67d",
  "spanId": "3fcb07029f32d740",
  "correlationId": "c5d1c321ad743f0abd05d55967adf67d",
  "requestId": "b36f4176-0e5b-42d7-8e17-772c0929e4d8",
  "sourceService": "traefik",
  "method": "GET",
  "url": "/api/health/live",
  "status": 200,
  "durationMs": 1,
  "msg": "HTTP request"
}

📝 Monitoring Integration: JSON logs can be streamed directly to Elasticsearch, Loki or other aggregators. The traceId/correlationId enables request tracking across all container boundaries — e.g. from Traefik through Backend to Worker.

Production Best Practices

  1. Secret Management: Keep secrets in .env (not in Git) or pass them in from an orchestrator (e.g. Kubernetes secrets) as environment variables
  2. Resource Limits: Pre-configured for all 12 containers (deploy.resources) — adjust to your hardware if needed
  3. Health Checks: All 12 containers have health checks configured
  4. Logging: Central logging system (ELK, Loki) for production
  5. Backup: Regular backups of postgres_data, uploads (see Docker Compose Details)
  6. Scaling: Workers can be scaled horizontally (see Scaling)
  7. Security: All secrets MUST be changed (JWT_SECRET, DB passwords, API keys, Redis password)
  8. Network: Docker Compose creates a dedicated network for the stack; allow only ports 80/443 from outside via firewall
  9. Updates: ClamAV updates itself (Freshclam), other containers: manual

Related Documentation