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 |
|---|---|---|---|
traefik | traefik healthcheck --ping | 15s | 10s |
frontend | wget -qO- http://localhost:80/health | 15s | 10s |
backend | node fetch /api/health/live | 15s | 300s |
db | pg_isready -U helpdesk_user -d helpdesk_db | 15s | 30s |
redis | redis-cli -a PASSWORD ping | grep PONG | 15s | 10s |
email-worker | node dist/healthcheck.js | 15s | 30s |
job-worker | node fetch /health/live (port 3001) | 15s | 30s |
workflow-engine | node fetch /health/live (port 3003) | 15s | 30s |
notification-worker | node dist/healthcheck.js | 15s | 30s |
report-generator | node fetch /health/live (port 3004) | 15s | 30s |
clamav | clamdcheck.sh | 60s | 180s |
av-worker | node dist/healthcheck.js | 15s | 30s |
Volumes & Persistence
Named Volumes
| Volume | Purpose | Size (estimated) |
|---|---|---|
postgres_data | PostgreSQL data (CRITICAL!) | 10-100GB (depends on data) |
redis_data | Redis AOF files (cache/queue) | 100MB-1GB |
uploads | User uploads (attachments) | 1GB-1TB (depends on usage) |
quarantine | Infected files (isolated) | < 100MB (rare) |
clamav_data | Virus definitions (daily updates) | 500MB-1GB |
sourcemaps | Frontend 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)