/

Developer Guide

Docker Compose Orchestration

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.


The shape of the configuration

Compose configuration is split across three kinds of file.

FileRole
docker-compose.yml (root)Shared infrastructure only — the gateway, broker, observability, and the Langfuse stack. It defines no application service.
backend/<service>/docker-compose.ymlOne per service: the service container and its private database/network.
backend/<service>/docker-compose.dev.ymlOptional 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:

Why services are not in the root file

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.


The orchestrator

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 &lt;up|down|build&gt;"]

Why a generated include: file, not multiple -f flags

Passing 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).

Do not commit the runtime file

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.

How the include list is built

  1. Always include the root docker-compose.yml.
  2. Walk backend/ (skipping tests/) and simulators/, adding each folder’s docker-compose.yml and — in development modes — its docker-compose.dev.yml if present.
  3. Add frontend unless excluded.
  4. For the integration mode, also include backend/tests/docker-compose.integration.yml.

Excluding services

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-simulator

The down, integration, and build modes always operate on the full set and ignore exclude patterns.


The modes

The first argument to the orchestrator selects the docker compose invocation. Each maps to a just recipe.

RecipeModeResulting command
just stack devdevup --watch --remove-orphans — hot-reload; rebuilds/syncs containers as source changes.
just stack dev-builddev-buildup --watch --build — as dev, but forces an image rebuild first.
just stack startstartup --build -d --remove-orphans — production-style, detached, in the background.
just stack downdowndown --remove-orphans — stop and remove everything.
just test integrationintegrationup --build --exit-code-from integration-tests --abort-on-container-exit.
(CI) compose-run.mjs buildbuildbuild 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.

Same assembly in CI

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.


Environment and the dev loop

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 down

just stack logs [service] follows logs against the generated compose.runtime.yml, so it targets exactly the services the last run started.


RecipePurpose
just db clearDrop the chat, twin, notification, and agent databases (MongoDB plus the agent’s Postgres).
just db clear-sensorDrop only the sensor database.
just test integrationBring 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.