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 |
traefik | 80, 443, 8082 | Traefik v3.7 | API gateway, TLS termination, routing, rate limiting |
frontend | 80 (internal) | Nginx + React 19 | Web UI, static SPA files |
backend | 3000 | Node.js 24 + Express | REST API, business logic |
postgres | 5432 (internal) | PostgreSQL 17 | Primary database |
redis | 6379 (internal) | Redis 8 | Cache, queues, pub/sub |
clamav | 3310 | ClamAV 1.5.1 | Virus scan daemon |
email-worker | 3005 | Node.js 20 | Email processing (IMAP/SMTP/Graph API) |
notification-worker | 3006 | Node.js 20 | Multi-channel notifications |
job-worker | 3001 | Node.js 24 | CronJobs, SLA monitor |
av-worker | 3007 | Node.js 24 | Virus scan coordination |
workflow-engine | 3003 | Node.js 20 | Workflow execution |
report-generator | 3004 | Node.js 24 | Report 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 | ✅ | ✅ | ✅ ALL | ✅ | 1.5G / 1C | ✅ Custom |
| Email-Worker | ✅ | ✅ | ✅ ALL | ✅ | 512M / 1C | ✅ Custom |
| Job-Worker | ✅ | ✅ | ✅ ALL | ✅ | 1G / 1C | ✅ HTTP |
| Notification-Worker | ✅ | ✅ | ✅ ALL | ✅ | 512M / 1C | ✅ Custom |
| Workflow-Engine | ✅ | ✅ | ✅ ALL | ✅ | 512M / 1C | ✅ HTTP |
| Report-Generator | ✅ | ✅ | ✅ ALL | ✅ | 1.5G / 1C | ✅ HTTP |
| Frontend | ✅ | — | ✅ 3 caps | ✅ | 256M / 0.5C | ✅ HTTP |
| Traefik | Partial (ro) | — | — | ✅ | 256M / 1C | ✅ Ping |
| ClamAV | ❌ | ❌ | ✅ 3 caps | ✅ | 2G / 2C | ✅ Script |
| Backend | ❌ | ❌ | ✅ 3 caps | ✅ | 2G / 2C | ✅ HTTP |
| PostgreSQL | ❌ | ❌ | ✅ 2 caps | ✅ | 2G / 2C | ✅ pg_isready |
| Redis | ❌ | ❌ | ✅ 2 caps | ✅ | 1G / 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 |
| traefik | 256MB | 1.0 | 64MB |
| frontend | 256MB | 0.5 | 32MB |
| backend | 2GB | 2.0 | 512MB |
| db (PostgreSQL) | 2GB | 2.0 | 256MB |
| redis | 1GB | 1.0 | 128MB |
| email-worker | 512MB | 1.0 | 128MB |
| job-worker | 1GB | 1.0 | 128MB |
| workflow-engine | 512MB | 1.0 | 128MB |
| notification-worker | 512MB | 1.0 | 128MB |
| ClamAV | 2GB | 2.0 | 512MB |
| AV-Worker | 1.5GB | 1.0 | 256MB |
| report-generator | 1.5GB | 1.0 | 256MB |
💡 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 |
| Traefik | Ping | traefik healthcheck --ping | 15s | 10s |
| Frontend | HTTP | wget localhost:80/health | 15s | 10s |
| Backend | HTTP | localhost:3000/api/health/live | 15s | 300s |
| PostgreSQL | CMD-SHELL | pg_isready -U helpdesk_user -d helpdesk_db | 15s | 30s |
| Redis | CMD-SHELL | redis-cli -a PASSWORD ping | grep PONG | 15s | 10s |
| ClamAV | Script | clamdcheck.sh | 60s | 180s |
| Email-Worker | Custom | node dist/healthcheck.js | 15s | 30s |
| Job-Worker | HTTP | localhost:3001/health/live | 15s | 30s |
| Notification-Worker | Custom | node dist/healthcheck.js | 15s | 30s |
| AV-Worker | Custom | node dist/healthcheck.js | 15s | 30s |
| Workflow-Engine | HTTP | localhost:3003/health/live | 15s | 30s |
| Report-Generator | HTTP | localhost:3004/health/live | 15s | 30s |
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 |
| Backend | JSON (Pino) | - | traceId, spanId, correlationId, requestId, sourceService |
| Job-Worker | JSON (Pino) | - | traceId, spanId, correlationId |
| Email-Worker | JSON (Pino) | - | traceId, spanId, correlationId |
| Notification-Worker | JSON (Pino) | - | traceId, spanId, correlationId |
| Workflow-Engine | JSON (Pino) | - | traceId, spanId, correlationId |
| AV-Worker | JSON (Pino) | 10MB x 3 | traceId, spanId, correlationId |
| Report-Generator | JSON (Pino) | - | traceId, spanId, correlationId |
| Traefik | JSON (Access-Log) | - | X-Request-ID, User-Agent |
| ClamAV | Native format | 10MB x 3 | - |
| PostgreSQL | Native format | - | - |
| Redis | Native 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
- Secret Management: Keep secrets in .env (not in Git) or pass them in from an orchestrator (e.g. Kubernetes secrets) as environment variables
- Resource Limits: Pre-configured for all 12 containers (deploy.resources) — adjust to your hardware if needed
- Health Checks: All 12 containers have health checks configured
- Logging: Central logging system (ELK, Loki) for production
- Backup: Regular backups of postgres_data, uploads (see Docker Compose Details)
- Scaling: Workers can be scaled horizontally (see Scaling)
- Security: All secrets MUST be changed (JWT_SECRET, DB passwords, API keys, Redis password)
- Network: Docker Compose creates a dedicated network for the stack; allow only ports 80/443 from outside via firewall
- Updates: ClamAV updates itself (Freshclam), other containers: manual
Related Documentation