# 911records.org app for Dokploy — EXTERNAL services variant, modeled on # ~/Code/prospect/docker-compose.yml: this app runs HA across TWO Dokploy # instances (StudioMac and MonsterMac) behind one Cloudflare tunnel, with # Dokploy auto-deploying this compose from `main` on each. That rules out a # bind mount to data/ (there is no single host both replicas share) — every # dependency here is a network service: # - Postgres (DATABASE_URL / DATABASE_READ_URL): schema `site`, built by # the pipeline (workstream A2); schema `app`, owned by this app. # - OpenSearch and the `files` service (PDFs, page images, word-box # files): both run on StudioMac's HOST docker, NOT in this compose — see # docker-compose.host.yml. Dokploy never builds or starts that file. services: app: # Shared Dokploy VM: contain runaway traffic after the 2026-10-05 crawl incident. mem_limit: 3g memswap_limit: 3g cpus: 2 build: context: ./web dockerfile: ../Dockerfile args: NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://911records.org} # The Google Analytics measurement id, and the off switch for the tag # (modeled on ~/Code/prospect's issue #982). No `:-` here, # deliberately: unlike NEXT_PUBLIC_SITE_URL above, this ARG has a # NON-EMPTY default in the Dockerfile (the live property id), and # `${NEXT_PUBLIC_GA_ID:-}` would render an explicit "" that OVERRIDES # that default — turning analytics off on the next deploy of both # instances. The bare form omits the arg entirely when the variable # is unset, so UNSET MEANS UNCHANGED. To turn analytics off, set this # to `off` (empty also works, but an empty value has to survive the # Dokploy UI, the project .env and compose interpolation, and a layer # that drops it gives back the very defect #982 fixed). `off` and # empty are the only two off-values; anything else is treated as a # measurement id. Both instances or neither, as above. NEXT_PUBLIC_GA_ID: # The commit this image was built from, reported by /api/health. # `:-` and never `:?` — a build with none set must still deploy and # report `"commit": null` rather than a guessed value. GIT_SHA: ${GIT_SHA:-} environment: NODE_OPTIONS: --max-old-space-size=2048 # Primary Postgres (writes, and reads with no standby configured). # e.g. postgres://sept11:@192.168.200.51:5433/sept11 DATABASE_URL: ${DATABASE_URL:?set the Postgres connection string} # Optional streaming standby for read traffic (lib/db.ts falls back to # the primary except on SQL syntax errors). Unset = reads use the primary. DATABASE_READ_URL: ${DATABASE_READ_URL:-} # This instance's stable name, reported by /api/health as `replica` — # set per Dokploy instance (e.g. `studiomac`, `monstermac`), same # pattern as prospect's REPLICA_NAME. REPLICA_NAME: ${REPLICA_NAME:-} # The `files` service and OpenSearch both run on docker-compose.host.yml # (StudioMac's host docker), reachable at its LAN IP from either Dokploy # instance. TURNSTILE_SITE_KEY: ${TURNSTILE_SITE_KEY:-} TURNSTILE_SECRET_KEY: ${TURNSTILE_SECRET_KEY:-} DOWNLOAD_SESSION_SECRET: ${DOWNLOAD_SESSION_SECRET:-} FILES_URL: ${FILES_URL:?set the files service URL, e.g. http://192.168.200.51:8911} # Plain HTTP: docker-compose.host.yml's OpenSearch has the security # plugin ON (basic auth required) but plugins.security.ssl.http.enabled # OFF — a LAN-only service, so there's no cert to trust and no need # for a NODE_TLS_REJECT_UNAUTHORIZED workaround. See that file. OPENSEARCH_URL: ${OPENSEARCH_URL:?set the OpenSearch URL, e.g. http://192.168.200.51:9200} OPENSEARCH_USER: ${OPENSEARCH_USER:-admin} OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-} # Query embedding for hybrid search. Routed through the files service's # /ollama/ proxy in production so Ollama itself never has to listen on # the LAN — see docker-compose.host.yml. OLLAMA_URL: ${OLLAMA_URL:-http://127.0.0.1:11434} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} ASK_MODEL: ${ASK_MODEL:-claude-haiku-4-5} ASK_DAILY_USD_CAP: ${ASK_DAILY_USD_CAP:-} GIT_SHA: ${GIT_SHA:-} restart: unless-stopped healthcheck: test: ['CMD-SHELL', 'wget -qO- http://127.0.0.1:3000/api/health || exit 1'] interval: 15s timeout: 5s retries: 3 start_period: 20s # Dokploy's Traefik attaches to this port; no bind mounts, no other # services — see docker-compose.host.yml for everything data-shaped. expose: - '3000'