services: redis: image: redis:7-alpine ports: - "6379:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 pgbouncer: # Connection pooler in front of Postgres for application traffic (DATABASE_URL). # Apps point DATABASE_URL at pgbouncer:6432, DIRECT_URL stays on postgres:5432 # so Prisma migrate can keep prepared statements. See docs/ops/pgbouncer.md. image: edoburu/pgbouncer:latest # No `ports:` mapping — pgbouncer must only be reachable inside the Docker # network. Public-facing 6432 would expose the pool to the open internet. volumes: - ./infra/pgbouncer/pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini:ro - ./infra/pgbouncer/userlist.txt:/etc/pgbouncer/userlist.txt:ro healthcheck: # pg_isready is shipped inside the edoburu image; queries the pool listener # without holding a server-side connection. test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-p", "6432", "-U", "pgb_admin"] interval: 10s timeout: 3s retries: 5 start_period: 10s restart: unless-stopped clamav: image: clamav/clamav:stable volumes: - clamav-db:/var/lib/clamav ports: - "3310:3310" healthcheck: # CLAMD_STARTUP_TIMEOUT only matters for the first boot — freshclam pulls # ~250MB of signatures before clamd starts listening. Allow ~20 minutes # for cold starts; after that the check passes in <2s. test: ["CMD", "clamdscan", "--ping", "1"] interval: 30s timeout: 5s retries: 40 start_period: 1200s restart: unless-stopped # Worker is split into five runtime variants — same image, same code, # different WORKER_QUEUE_GROUP env. See docs/ops/worker-queues.md. # `worker` is kept as a YAML anchor so the five group services share a # single build context. Only worker-general depends on ClamAV because it # owns the file-scan queue; other worker groups override depends_on to avoid # blocking on first-boot signature downloads. worker-general: &worker-base build: context: . target: worker environment: - WORKER_ENABLED=${WORKER_ENABLED:-false} - WORKER_QUEUE_GROUP=general - WORKER_CONCURRENCY_GENERAL=${WORKER_CONCURRENCY_GENERAL:-10} - WORKER_HEALTH_PORT=9101 - WORKER_ADMIN_PORT=9201 - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - DATABASE_URL=${DATABASE_URL:-} - APP_URL=${APP_URL:-} - OUTBOX_DISPATCHER_ENABLED=${OUTBOX_DISPATCHER_ENABLED:-false} - OUTBOX_DISPATCHER_REQUIRED=${OUTBOX_DISPATCHER_REQUIRED:-false} - OUTBOX_DISPATCH_CONCURRENCY=${OUTBOX_DISPATCH_CONCURRENCY:-10} - WORKFLOW_TRIGGER_EXECUTOR_MODE=${WORKFLOW_TRIGGER_EXECUTOR_MODE:-http} - WORKFLOW_TRIGGER_EXECUTOR_URL=${WORKFLOW_TRIGGER_EXECUTOR_URL:-} - API_ACCESS_TOKEN=${API_ACCESS_TOKEN:-} - WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS=${WORKFLOW_TRIGGER_EXECUTOR_TIMEOUT_MS:-30000} - WORKFLOW_TRIGGER_OUTBOX_ENABLED=${WORKFLOW_TRIGGER_OUTBOX_ENABLED:-true} - WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY=${WORKFLOW_TRIGGER_OUTBOX_CONCURRENCY:-5} - WORKFLOW_TRIGGER_OUTBOX_REQUIRED=${WORKFLOW_TRIGGER_OUTBOX_REQUIRED:-false} - SCAN_QUEUE_ENABLED=${SCAN_QUEUE_ENABLED:-false} - CLAMAV_HOST=${CLAMAV_HOST:-clamav} - CLAMAV_PORT=${CLAMAV_PORT:-3310} - CLAMAV_TIMEOUT_MS=${CLAMAV_TIMEOUT_MS:-30000} depends_on: redis: condition: service_healthy clamav: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9101/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s worker-mail: <<: *worker-base environment: - WORKER_ENABLED=${WORKER_ENABLED:-false} - WORKER_QUEUE_GROUP=mail - WORKER_CONCURRENCY_MAIL=${WORKER_CONCURRENCY_MAIL:-5} - WORKER_HEALTH_PORT=9102 - WORKER_ADMIN_PORT=9202 - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - DATABASE_URL=${DATABASE_URL:-} depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9102/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s worker-import: <<: *worker-base environment: - WORKER_ENABLED=${WORKER_ENABLED:-false} - WORKER_QUEUE_GROUP=import - WORKER_CONCURRENCY_IMPORT=${WORKER_CONCURRENCY_IMPORT:-1} - WORKER_HEALTH_PORT=9103 - WORKER_ADMIN_PORT=9203 - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - DATABASE_URL=${DATABASE_URL:-} depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9103/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s worker-webhook: <<: *worker-base environment: - WORKER_ENABLED=${WORKER_ENABLED:-false} - WORKER_QUEUE_GROUP=webhook - WORKER_CONCURRENCY_WEBHOOK=${WORKER_CONCURRENCY_WEBHOOK:-20} - WORKER_HEALTH_PORT=9104 - WORKER_ADMIN_PORT=9204 - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - DATABASE_URL=${DATABASE_URL:-} depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9104/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s worker-plugin: <<: *worker-base environment: - WORKER_ENABLED=${WORKER_ENABLED:-false} - WORKER_QUEUE_GROUP=plugin - WORKER_CONCURRENCY_PLUGIN=${WORKER_CONCURRENCY_PLUGIN:-5} - WORKER_HEALTH_PORT=9105 - WORKER_ADMIN_PORT=9205 - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - DATABASE_URL=${DATABASE_URL:-} depends_on: redis: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:9105/healthz || exit 1"] interval: 30s timeout: 5s retries: 3 start_period: 20s # Meilisearch — full-text search engine consumed by packages/search. # Indexing is driven asynchronously by the worker-general service via # outbox events; tenant isolation is enforced inside the wrapper. The # master key is dev-only (`devkey`); production must inject a rotated # secret. See docs/deploy/meilisearch.md. meilisearch: image: getmeili/meilisearch:v1.8 environment: - MEILI_MASTER_KEY=${MEILI_MASTER_KEY:-devkey} - MEILI_ENV=${MEILI_ENV:-development} - MEILI_NO_ANALYTICS=true ports: # Bind to loopback only: the default master key is the well-known `devkey`, # so the search engine must never be reachable from the LAN/host network. # Production injects a rotated MEILI_MASTER_KEY via its own compose/secrets. - "127.0.0.1:7700:7700" volumes: - meilisearch-data:/meili_data healthcheck: test: ["CMD-SHELL", "wget -q -O - http://127.0.0.1:7700/health | grep -q 'available'"] interval: 10s timeout: 5s retries: 6 start_period: 15s restart: unless-stopped # Hocuspocus — realtime-collaboration WebSocket server backing the # Novel-Editor (Y.js CRDT). Auth is JWT-signed via HOCUSPOCUS_AUTH_SECRET # (issued by the web app's collab-token endpoint). Disabled by default — # set HOCUSPOCUS_ENABLED=true in `.env` to opt in. See # docs/features/realtime-collab.md. hocuspocus: build: context: . dockerfile: apps/hocuspocus/Dockerfile target: hocuspocus environment: - NODE_ENV=${NODE_ENV:-production} - HOCUSPOCUS_PORT=1234 - HOCUSPOCUS_HOST=0.0.0.0 - HOCUSPOCUS_AUTH_SECRET=${HOCUSPOCUS_AUTH_SECRET:-} - DATABASE_URL=${DATABASE_URL:-} - DIRECT_URL=${DIRECT_URL:-} ports: - "1234:1234" profiles: - realtime healthcheck: test: - "CMD-SHELL" - "node -e \"require('net').connect(1234,'127.0.0.1').on('connect',()=>process.exit(0)).on('error',()=>process.exit(1))\"" interval: 30s timeout: 5s retries: 3 start_period: 15s restart: unless-stopped volumes: clamav-db: meilisearch-data: