Skip to content

Local Stack (Docker)

Each repo ships a self-contained local-setup/ folder — a docker-compose.yml, a .env.example, and a LOCAL_SETUP.md guide — that brings up the DPG and its backing services (Postgres, Redis, Keycloak, Mailpit, and - for the Aggregator - MinIO). You don’t wire the infra by hand; you copy an env file and run one command.

Keycloak is shared: both DPGs authenticate against the same realm — one per network, named by KEYCLOAK_REALM (bluedots by default locally) — so it must be running and the realm imported before either UI will let you sign in.

Both offer the same two tracks:

  • Track A — Docker-only: one docker compose up -d --build. Fastest way to explore.
  • Track B — hybrid dev: backing services in Docker, apps from source with hot-reload.

Both also offer an opt-in search profile that adds relevance ranking — see Search and relevance ranking below.

I want to…UseGuide
Run Signals only (backend + UI)signals-dpg/local-setup/Signals DPG Setup
Run the full ecosystem (Aggregator + Signals)aggregator-dpg/local-setup/Aggregator DPG Setup

The Aggregator’s local-setup/ builds both repos, so clone them as siblings under one parent directory:

<parent>/
├── aggregator-dpg/ # full-ecosystem stack lives in aggregator-dpg/local-setup/
│ └── local-setup/
└── signals-dpg/ # standalone stack lives in signals-dpg/local-setup/
└── local-setup/
ServicePortIn which stack
Aggregator portal3100Aggregator (full)
Signals UI5173both
Aggregator API4000Aggregator (full)
Signals API2742both
Keycloak8080both
Mailpit (email UI)8025both
MinIO (S3 API)9000Aggregator (full)
MinIO console9001Aggregator (full)
Postgres5432both
Redis5555 / 6379Signals / Aggregator
Signals Search API3100 (Signals) / 3110 (Aggregator)both, only with --profile search

The Search API’s host port differs between the two stacks: in the Aggregator’s unified stack the portal already occupies 3100, so search is published on 3110 instead. Inside either compose network the service still listens on 3100, so container-to-container URLs are identical.

By default neither stack runs signals-search. Everything works, but discover returns results in recency order rather than by relevance, and match scores are unavailable. The UI says so explicitly when it happens — “Showing basic matches — relevance ranking is temporarily unavailable”.

To add it:

Terminal window
# from whichever local-setup/ you are using
cp .env.search.example .env.search # then mint an apikey — see the repo guide
docker compose --profile search up -d

That brings up three more services: the search query API, an ingestion worker that keeps the search index current, and a TEI embedding server with BAAI/bge-m3 baked in. All are pulled prebuilt from public GHCR, so there is no extra checkout and no registry login.

Two things catch people out, and both are documented in full in the repo guides:

  • Search authenticates against the apikey table in the shared Signals database. There is no API-key environment variable to set — the key has to be a real, enabled row, minted with the service-apikey seed step.
  • Wiring Signals to search takes two separate variable sets. Relevance-ranked discover reads SIGNALS_SEARCH_URL; match score reads SIGNALS_SEARCH_ENDPOINT — the same URL under a different name — plus MATCH_SCORE_PROVIDER=signals_search. Setting only one gives you working discover and a broken match score, or the reverse.

Full reference, including the embedding-dimension rules and a troubleshooting table keyed by the exact error message: §7 of signals-dpg/local-setup/LOCAL_SETUP.md (canonical), and §10 of the Aggregator’s guide for what differs in the unified stack.

Next: set up each DPG — Signals or the Aggregator.