# DOMINUS — Docker Compose for development and community deployment. # # Community edition (SQLite, single-user): all services start by default. # docker compose up -d # # DOMINUS Cloud (PostgreSQL, multi-tenant): use the production overlay, # which adds postgres, redis and the monitoring stack: # docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d # # Production (with .env file for secrets): # docker compose --env-file .env up -d # # Service note: # api + worker + scheduler start by default — the job-queue worker and # the background scheduler are required for async runs, backups and # renewal checks (ADR-0023). This file intentionally has no # PostgreSQL/Redis: the community edition runs on SQLite at €0 infra # cost. The production overlay adds the data stores and monitoring. services: api: build: context: . dockerfile: Dockerfile target: api image: dominus-api:latest ports: # Bind to loopback by default: the API is unauthenticated on / # health only, and exposing the app port on the host network # without a reverse proxy is a firewall dependency the operator # must opt into explicitly. Override with 0.0.0.0:${PORT}:3000 # when a LAN-exposed deployment is intended. - "127.0.0.1:${PORT:-3000}:3000" volumes: # DATA_DIR lets the local Docker gate run against a throwaway database # instead of the developer's ./data (a stale schema there makes the # smoke fail for reasons that have nothing to do with the image). - ${DATA_DIR:-./data}:/app/data environment: - NODE_ENV=${NODE_ENV:-production} - DATABASE_PATH=/app/data/dominus.db - DATABASE_URL=${DATABASE_URL:-} - PORT=${PORT:-3000} - HOST=0.0.0.0 - LOG_LEVEL=${LOG_LEVEL:-info} - LOG_PRETTY=${LOG_PRETTY:-false} - KEYWORD_DATA_PATH=${KEYWORD_DATA_PATH:-} - COMPS_DATA_PATH=${COMPS_DATA_PATH:-} - EUIPO_CLIENT_ID=${EUIPO_CLIENT_ID:-} - EUIPO_CLIENT_SECRET=${EUIPO_CLIENT_SECRET:-} - USPTO_SEARCH_URL=${USPTO_SEARCH_URL:-} - EUIPO_AUTH_URL=${EUIPO_AUTH_URL:-} - EUIPO_API_URL=${EUIPO_API_URL:-} - TM_CACHE_TTL_DAYS=${TM_CACHE_TTL_DAYS:-7} - SCORING_WEIGHTS_OVERRIDE=${SCORING_WEIGHTS_OVERRIDE:-} - DNS_BULK_CONCURRENCY=${DNS_BULK_CONCURRENCY:-10} - WHOIS_LOOKUP_TIMEOUT=${WHOIS_LOOKUP_TIMEOUT:-10000} - BUY_MAX_ABSOLUTE_CAP=${BUY_MAX_ABSOLUTE_CAP:-0} - TRUST_PROXY_DEPTH=${TRUST_PROXY_DEPTH:-0} - FRONTEND_DIST_PATH=./dist/web # Boot-required: HOST below is 0.0.0.0, so the API refuses to start # without authentication (fail-closed guard in src/index.ts). Without # this guard the api container crash-loops through the ~30 s Unbound # readiness wait on every restart, which looks like a hang to an # operator. Docker Compose resolves this :? guard and aborts `up -d` # with the fix in the message. The message intentionally contains no # "=" and no shell substitution: Compose parses list-form environment # entries by splitting on the first "=", so either would corrupt it. - API_KEYS=${API_KEYS:?API_KEYS is required. Generate one with openssl rand -hex 32, then export it as admin=} - DROP_SCORE_THRESHOLD=${DROP_SCORE_THRESHOLD:-25} - DROP_RENEWAL_HORIZON_DAYS=${DROP_RENEWAL_HORIZON_DAYS:-60} - WORKER_ENABLED=false - SCHEDULER_ENABLED=false - REDIS_URL=${REDIS_URL:-} - PURCHASE_AUTO_APPROVAL=${PURCHASE_AUTO_APPROVAL:-never} # Unbound via the docker-compose.unbound.yml overlay (ADR-0072/0075). # The overlay provides the Unbound sidecar and injects # DNS_UNBOUND_HOSTS=unbound:5300 plus DNS_UNBOUND_STRICT=true. # Without the overlay, DNS_UNBOUND_STRICT falls back to its # context-aware default (ADR-0076): community edition uses the # Node.js fallback for non-Available verdicts, cloud stays strict. # Fallback never serves Available verdicts (ADR-0002 conservatism). - DNS_UNBOUND_ENABLED=true - DNSSEC_MODE=strict logging: driver: "json-file" options: max-size: "10m" max-file: "3" compress: "true" restart: unless-stopped worker: build: context: . dockerfile: Dockerfile target: worker image: dominus-worker:latest depends_on: api: condition: service_healthy volumes: - ${DATA_DIR:-./data}:/app/data - backups:/backups environment: - NODE_ENV=${NODE_ENV:-production} - DATABASE_PATH=/app/data/dominus.db - DATABASE_URL=${DATABASE_URL:-} - LOG_LEVEL=${LOG_LEVEL:-info} - KEYWORD_DATA_PATH=${KEYWORD_DATA_PATH:-} - COMPS_DATA_PATH=${COMPS_DATA_PATH:-} - EUIPO_CLIENT_ID=${EUIPO_CLIENT_ID:-} - EUIPO_CLIENT_SECRET=${EUIPO_CLIENT_SECRET:-} - USPTO_SEARCH_URL=${USPTO_SEARCH_URL:-} - EUIPO_AUTH_URL=${EUIPO_AUTH_URL:-} - EUIPO_API_URL=${EUIPO_API_URL:-} - TM_CACHE_TTL_DAYS=${TM_CACHE_TTL_DAYS:-7} - SCORING_WEIGHTS_OVERRIDE=${SCORING_WEIGHTS_OVERRIDE:-} - DNS_BULK_CONCURRENCY=${DNS_BULK_CONCURRENCY:-10} - WHOIS_LOOKUP_TIMEOUT=${WHOIS_LOOKUP_TIMEOUT:-10000} - BUY_MAX_ABSOLUTE_CAP=${BUY_MAX_ABSOLUTE_CAP:-0} - DROP_SCORE_THRESHOLD=${DROP_SCORE_THRESHOLD:-25} - DROP_RENEWAL_HORIZON_DAYS=${DROP_RENEWAL_HORIZON_DAYS:-60} - WORKER_ENABLED=true - SCHEDULER_ENABLED=false - WORKER_CONCURRENCY=${WORKER_CONCURRENCY:-2} - JOB_QUEUE_POLL_INTERVAL_MS=${JOB_QUEUE_POLL_INTERVAL_MS:-1000} - BACKUP_DIR=${BACKUP_DIR:-/backups} - REDIS_URL=${REDIS_URL:-} # Unbound via the docker-compose.unbound.yml overlay (ADR-0072/0075). # The overlay provides the Unbound sidecar and injects # DNS_UNBOUND_HOSTS=unbound:5300 plus DNS_UNBOUND_STRICT=true. # Without the overlay, DNS_UNBOUND_STRICT falls back to its # context-aware default (ADR-0076): community edition uses the # Node.js fallback for non-Available verdicts, cloud stays strict. # Fallback never serves Available verdicts (ADR-0002 conservatism). - DNS_UNBOUND_ENABLED=true - DNSSEC_MODE=strict logging: driver: "json-file" options: max-size: "10m" max-file: "3" compress: "true" restart: unless-stopped scheduler: build: context: . dockerfile: Dockerfile target: scheduler image: dominus-scheduler:latest depends_on: api: condition: service_healthy volumes: - ${DATA_DIR:-./data}:/app/data - backups:/backups environment: - NODE_ENV=${NODE_ENV:-production} - DATABASE_PATH=/app/data/dominus.db - DATABASE_URL=${DATABASE_URL:-} - LOG_LEVEL=${LOG_LEVEL:-info} - KEYWORD_DATA_PATH=${KEYWORD_DATA_PATH:-} - COMPS_DATA_PATH=${COMPS_DATA_PATH:-} - EUIPO_CLIENT_ID=${EUIPO_CLIENT_ID:-} - EUIPO_CLIENT_SECRET=${EUIPO_CLIENT_SECRET:-} - USPTO_SEARCH_URL=${USPTO_SEARCH_URL:-} - EUIPO_AUTH_URL=${EUIPO_AUTH_URL:-} - EUIPO_API_URL=${EUIPO_API_URL:-} - TM_CACHE_TTL_DAYS=${TM_CACHE_TTL_DAYS:-7} - SCORING_WEIGHTS_OVERRIDE=${SCORING_WEIGHTS_OVERRIDE:-} - DNS_BULK_CONCURRENCY=${DNS_BULK_CONCURRENCY:-10} - WHOIS_LOOKUP_TIMEOUT=${WHOIS_LOOKUP_TIMEOUT:-10000} - BUY_MAX_ABSOLUTE_CAP=${BUY_MAX_ABSOLUTE_CAP:-0} - DROP_SCORE_THRESHOLD=${DROP_SCORE_THRESHOLD:-25} - DROP_RENEWAL_HORIZON_DAYS=${DROP_RENEWAL_HORIZON_DAYS:-60} - WORKER_ENABLED=false - SCHEDULER_ENABLED=true - SCHEDULER_BACKUP_CRON=${SCHEDULER_BACKUP_CRON:-0 4 * * *} - SCHEDULER_PRUNE_CRON=${SCHEDULER_PRUNE_CRON:-0 10 1 * *} - BACKUP_DIR=${BACKUP_DIR:-/backups} - REDIS_URL=${REDIS_URL:-} # Unbound via the docker-compose.unbound.yml overlay (ADR-0072/0075). # The overlay provides the Unbound sidecar and injects # DNS_UNBOUND_HOSTS=unbound:5300 plus DNS_UNBOUND_STRICT=true. # Without the overlay, DNS_UNBOUND_STRICT falls back to its # context-aware default (ADR-0076): community edition uses the # Node.js fallback for non-Available verdicts, cloud stays strict. # Fallback never serves Available verdicts (ADR-0002 conservatism). - DNS_UNBOUND_ENABLED=true - DNSSEC_MODE=strict logging: driver: "json-file" options: max-size: "10m" max-file: "3" compress: "true" restart: unless-stopped # Dedicated backups volume: keeps VACUUM INTO/pg_dump backups off the # disk that holds dominus.db (isolation from DB write amplification and # from accidental wipes of ./data). The image pre-creates /backups owned # by the non-root dominus user. For off-disk durability, mount an # external drive at /backups instead (see docs/deployment/README.md). volumes: backups: