Developer Guide
Locally, the entire platform runs under Docker Compose. There is no single hand-maintained compose file that lists every service; instead, each service owns its own compose fragment, and a small Node orchestrator assembles them into one runtime file on demand. This page explains that mechanism end to end, so a new contributor can predict exactly what just stack dev does.
Compose configuration is split across three kinds of file.
| File | Role |
|---|---|
docker-compose.yml (root) | Shared infrastructure only — the gateway, broker, observability, and the Langfuse stack. It defines no application service. |
backend/<service>/docker-compose.yml | One per service: the service container and its private database/network. |
backend/<service>/docker-compose.dev.yml | Optional per-service development override (hot-reload watch rules, dev-only ports). |
The root file holds the pieces every run needs regardless of which services are included:
gateway — the Caddy reverse proxy on port 80 (see Architecture Overview. Its depends_on entries are marked required: false, so the gateway still starts when a service is excluded and simply retries the connection when that service appears.redis — the publish/subscribe broker.keycloak and keycloak-db — the hosted identity provider every login flows through, and its private Postgres. keycloak imports keycloak/realm-export.json on boot and exposes its own admin console directly on host port 8090 (not behind the gateway) — see Identity & Tenancy Architecture.prometheus and grafana — metrics and dashboards.langfuse-*, redis-langfuse, clickhouse, and minio stack — the agent’s trace backend (see Setting Up the Environment for the generated Langfuse credentials and UI URL).Keeping each service’s compose fragment next to its code means the fragment travels with the service if it is ever extracted to its own repository, and a service can be added or excluded without editing a central file. The trade-off — assembling the fragments correctly — is what the orchestrator handles.
scripts/compose/compose-run.mjs (invoked by the Justfile, never directly) turns the scattered fragments into one runnable configuration.
flowchart TD
J["just stack dev / start / down / …"] --> R["compose-run.mjs"]
R --> W["Walk backend/ and simulators/<br/>apply exclude patterns"]
W --> G["Write compose.runtime.yml<br/>(an include: list)"]
G --> D["docker compose -f compose.runtime.yml <up|down|build>"]include: file, not multiple -f flagsPassing several files with repeated -f flags makes Compose resolve every relative path (such as build.context: .) against the first file’s directory, which breaks per-service Dockerfile lookups. Compose’s include: directive instead resolves paths relative to each included file, so build.context: . inside backend/twin-service/docker-compose.yml correctly points at that service’s directory. The orchestrator therefore writes a small compose.runtime.yml whose body is an include: list:
# Auto-generated by scripts/compose/compose-run.mjs — do not commit.
include:
- path: docker-compose.yml
- path:
- backend/twin-service/docker-compose.yml
- backend/twin-service/docker-compose.dev.yml
- path: backend/sensor-service/docker-compose.yml
# … one entry per included service …A single-path entry becomes one sub-project; a multi-path entry merges a service’s base file with its dev override into one sub-project (two separate include entries cannot define the same service — that raises a conflict).
compose.runtime.yml is regenerated on every just stack dev / start / down / build. It is a build artifact, not source — treat it as disposable and never edit it by hand.
docker-compose.yml.backend/ (skipping tests/) and simulators/, adding each folder’s docker-compose.yml and — in development modes — its docker-compose.dev.yml if present.frontend unless excluded.backend/tests/docker-compose.integration.yml.In dev, dev-build, and start, you can omit services by substring of their folder name. This is how you run a lighter stack when you do not need the agent or the simulators:
just stack dev exclude="agent" # skip agent-service, agent-db, agent-ingester
just stack dev exclude="agent simulator" # also skip aq-simulator, sensor-simulatorThe down, integration, and build modes always operate on the full set and ignore exclude patterns.
The first argument to the orchestrator selects the docker compose invocation. Each maps to a just recipe.
| Recipe | Mode | Resulting command |
|---|---|---|
just stack dev | dev | up --watch --remove-orphans — hot-reload; rebuilds/syncs containers as source changes. |
just stack dev-build | dev-build | up --watch --build — as dev, but forces an image rebuild first. |
just stack start | start | up --build -d --remove-orphans — production-style, detached, in the background. |
just stack down | down | down --remove-orphans — stop and remove everything. |
just test integration | integration | up --build --exit-code-from integration-tests --abort-on-container-exit. |
(CI) compose-run.mjs build | build | build only — used by the ci-docker workflow to validate the whole stack builds. |
The orchestrator runs docker compose with COMPOSE_BAKE=true, which delegates builds to BuildKit Bake for faster, parallel image builds.
The ci-docker workflow builds the stack through the very same orchestrator (build mode). Path resolution in CI is therefore identical to local development, so “it builds on my machine” and “it builds in CI” cannot diverge over compose path handling.
Every stack-starting recipe (dev, dev-build, start) depends on the env recipe, which regenerates \.env (VAPID keys, the local-dev-only eval JWT secret, Langfuse secrets) before Compose runs — see Setting Up Your Environment.
Typical local loop:
just stack dev # full stack with hot-reload
just stack dev exclude="agent" # lighter stack without the AI agent
just stack logs twin-service # follow one service's logs
just stack down # tear it all downjust stack logs [service] follows logs against the generated compose.runtime.yml, so it targets exactly the services the last run started.
| Recipe | Purpose |
|---|---|
just db clear | Drop the chat, twin, notification, and agent databases (MongoDB plus the agent’s Postgres). |
just db clear-sensor | Drop only the sensor database. |
just test integration | Bring up the full stack plus the integration test container, exit with its status, then tear down. |
For the production-grade orchestration (StatefulSets, ingress, secrets), see Kubernetes Configuration; for the runtime topology and gateway routing, see the Architecture Overview.