networks: bettersight_default: # LOCAL DEV — CLAUDE.md §4 uses an external `pangolin_default` network # (Dokploy/Pangolin-managed, production only). Locally there is no # Pangolin reverse proxy, so this is a plain compose-managed bridge # network instead. Do not port this network block to production — # production still uses the external pangolin_default per CLAUDE.md. driver: bridge volumes: pocketbase_data: services: flask-api: build: ./backend container_name: flask-api command: gunicorn app:app -w 4 -b 0.0.0.0:5000 env_file: ./backend/.env environment: # Overrides backend/.env's values with container-network-correct # addresses. host.docker.internal reaches the PocketBase instance # already running on the host machine (per user instruction, that # instance is started manually and is NOT the pocketbase service # below — this compose file never starts its own PocketBase). - POCKETBASE_URL=http://host.docker.internal:8090 - REDIS_URL=redis://bettersight-redis:6379 - FIRECRAWL_URL=http://firecrawl-api:3002 ports: - "5050:5000" extra_hosts: - "host.docker.internal:host-gateway" networks: - bettersight_default restart: unless-stopped depends_on: - bettersight-redis bettersight-worker: build: ./backend container_name: bettersight-worker command: rq worker high normal low -u redis://bettersight-redis:6379 env_file: ./backend/.env environment: - POCKETBASE_URL=http://host.docker.internal:8090 - REDIS_URL=redis://bettersight-redis:6379 - FIRECRAWL_URL=http://firecrawl-api:3002 extra_hosts: - "host.docker.internal:host-gateway" networks: - bettersight_default restart: unless-stopped depends_on: - bettersight-redis bettersight-redis: image: redis:alpine container_name: bettersight-redis networks: - bettersight_default restart: unless-stopped # ── Opt-in only — real self-hosted Firecrawl is a 3-service stack # built from source (no stable published image — see CLAUDE.md §4's # "Firecrawl — real self-hosted stack" note). Builds from a local # clone at ./firecrawl (`git clone https://github.com/firecrawl/ # firecrawl.git`) rather than a remote git build context — this # environment's network couldn't reliably clone the repo itself # (git's smart-HTTP protocol specifically; plain HTTPS to GitHub's # API and to PyPI both worked fine, so this was not general # bandwidth throttling). Re-point back to the GitHub URL below if # building somewhere with normal git connectivity, and drop the # local clone from the repo (it's gitignored, not committed). # # Build context is the app subdirectory itself (./firecrawl/apps/api), # not the repo root — apps/api/Dockerfile's COPY instructions (e.g. # `COPY sharedLibs/go-html-to-md ...`) are written relative to # apps/api/, matching upstream firecrawl's own docker-compose.yaml # (`build: apps/api`). Pointing context at the repo root with # `dockerfile: apps/api/Dockerfile` resolves the Dockerfile from the # right place but still resolves every COPY inside it against the # repo root, not apps/api/ — breaks on the first COPY that assumes # otherwise. # Firecrawl's NUQ queue backend needs a real Postgres, not just Redis — # discovered only once firecrawl-api actually started: without # NUQ_DATABASE_URL set, harness.ts's setupNuqPostgres() tries to # auto-provision a Postgres container by shelling out to `docker`/ # `podman` from *inside* the firecrawl-api container itself, which # obviously has neither installed and isn't meant to (no # Docker-in-Docker here). Firecrawl ships a purpose-built Postgres # image for this at apps/nuq-postgres/ — pg_cron preloaded plus the # NUQ schema (nuq.sql) baked in as an initdb script — build that # rather than pointing at a vanilla postgres image, which would boot # without the schema NUQ expects. # # Setting POSTGRES_HOST to this container's name (anything other # than "localhost") is what flips setupNuqPostgres() into its # docker-compose branch, which just builds the connection URL # directly and skips the Docker/Podman auto-provision path entirely # — this is the code path Firecrawl's own authors intended for # exactly this setup, not a workaround. # # NUQ_RABBITMQ_URL turned out not to be optional in practice, despite # most call sites gracefully checking `if (config.NUQ_RABBITMQ_URL)` # and falling back to polling mode when absent. The harness always # spawns an extract-worker subprocess (dist/src/services/extract-worker.js) # regardless of which routes are actually used, and that subprocess's # consumeExtractJobs() throws immediately without it — no config # check gates whether it starts. The harness treats any subprocess # exit as fatal to the whole process, so without RabbitMQ the entire # firecrawl-api container crash-looped every ~2s even though # Bettersight never calls /extract. Upstream's own docker-compose.yaml # confirms this isn't optional either — it lists rabbitmq as a hard # `depends_on: condition: service_healthy` for the api service. Stock # image, no build needed. firecrawl-rabbitmq: image: rabbitmq:3-management container_name: firecrawl-rabbitmq profiles: ["firecrawl"] networks: - bettersight_default restart: unless-stopped firecrawl-nuq-postgres: build: context: ./firecrawl/apps/nuq-postgres container_name: firecrawl-nuq-postgres profiles: ["firecrawl"] environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres networks: - bettersight_default restart: unless-stopped # No separate firecrawl-worker service — checked upstream's own # docker-compose.yaml (firecrawl/docker-compose.yaml) and it defines # only one `api` container, command `node dist/src/harness.js # --start-docker`. That harness process spins up the queue workers # internally (NUQ_WORKER_COUNT etc — see apps/api/src/harness.ts) # rather than running as a separate process. A prior version of this # file ran a second container on `pnpm run workers`, which crash # looped: the runtime image only carries node_modules/dist/native # (no package.json — see the Dockerfile's runtime stage), so pnpm # had nothing to run against. This matches the older split-process # Firecrawl architecture CLAUDE.md's §4 was written against, not the # current one — do not reintroduce a second container here. firecrawl-api: build: context: ./firecrawl/apps/api # context: https://github.com/firecrawl/firecrawl.git#main&context=apps/api container_name: firecrawl-api profiles: ["firecrawl"] environment: - PORT=3002 - HOST=0.0.0.0 - REDIS_URL=redis://bettersight-redis:6379 - REDIS_RATE_LIMIT_URL=redis://bettersight-redis:6379 - PLAYWRIGHT_MICROSERVICE_URL=http://firecrawl-playwright:3000/scrape - USE_DB_AUTHENTICATION=false - NUM_WORKERS_PER_QUEUE=8 - POSTGRES_HOST=firecrawl-nuq-postgres - POSTGRES_PORT=5432 - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres - NUQ_RABBITMQ_URL=amqp://firecrawl-rabbitmq:5672 depends_on: - bettersight-redis - firecrawl-playwright - firecrawl-nuq-postgres - firecrawl-rabbitmq networks: - bettersight_default restart: unless-stopped firecrawl-playwright: build: context: ./firecrawl/apps/playwright-service-ts # context: https://github.com/firecrawl/firecrawl.git#main&context=apps/playwright-service-ts container_name: firecrawl-playwright profiles: ["firecrawl"] environment: - PORT=3000 networks: - bettersight_default restart: unless-stopped mem_limit: 4g # ── Opt-in only — excluded from a plain `docker compose up -d` ──────── # Started with `docker compose --profile with-pocketbase up -d # pocketbase` if this local host-run instance is ever retired in # favour of a containerized one. Per explicit instruction: defined # here for parity with CLAUDE.md §4, but never auto-started while a # host PocketBase is already running on localhost:8090. pocketbase: image: ghcr.io/muchobien/pocketbase:latest container_name: pocketbase profiles: ["with-pocketbase"] volumes: - pocketbase_data:/pb_data - ./backend/pb_migrations:/pb_migrations networks: - bettersight_default restart: unless-stopped # ── Opt-in only — needs MAIL_FROM/SMTP config not present locally ── freescout: image: freescout/freescout:latest container_name: freescout profiles: ["support"] environment: - APP_URL=http://localhost:8080 - MAIL_FROM=support@bettersight.io networks: - bettersight_default restart: unless-stopped # ── Opt-in only — needs Hetzner S3 credentials not present locally ── litestream: image: litestream/litestream:latest container_name: litestream profiles: ["backup"] command: replicate environment: - LITESTREAM_ACCESS_KEY_ID=${HETZNER_S3_ACCESS_KEY} - LITESTREAM_SECRET_ACCESS_KEY=${HETZNER_S3_SECRET_KEY} networks: - bettersight_default restart: unless-stopped