Running in Docker
make up # docker compose up -d — MCP + persistent state
make down # stop; your trial history survives
This is a local development stack, not an easier way to try TradeFlow. Docker is itself a prerequisite, so if you just want to run this, installing the command is strictly simpler:
uv tool install tradeflow-engine && tradeflow demo
What compose adds is the volume and environment wiring people get wrong by hand, a long-running MCP server and docs site, and the seam a database service would attach to later without changing how anything boots.
Honest framing: TradeFlow is a single-process application with file-backed state, so most of the compose file is volumes and named entrypoints rather than service orchestration.
up never starts trading
The default up boots research-clock surfaces only. live sits behind its own
profile and is not in the default service set — turning the machine on must not turn
trading on, not even paper trading:
docker compose run --rm live # the only way to start it
The image's default command is --help, not a trading loop. PAPER_TRADE is still
asserted inside the container, so the profile is the outer of two locks rather than
a replacement for one.
One-shot commands
Same image, same state, different verb — arguments pass straight through:
docker compose run --rm demo
docker compose run --rm verdict --symbols NVDA,AAPL,META --start 2024-01-01 --end 2024-12-31
docker compose run --rm backtest --symbols NVDA --start 2024-01-01 --end 2024-06-01
docker compose run --rm trials best
Or via the Makefile: make compose-run CMD="verdict --symbols NVDA".
The documentation site runs under its own profile:
docker compose --profile docs up docs # http://localhost:3000
State, and why the volumes matter
Three named volumes hold everything the application writes — the research journal
and trial store (/state/logs), the bar cache (/state/cache), and promoted
configs (/state/configs). .env is bind-mounted read-only from the host: a
container should not be able to rewrite the keys it was given.
This is the part worth getting right. A compose setup that looks fine while
journaling trials into the container layer would discard them on down, silently
resetting the campaign count that every multiple-testing correction depends on — and
nothing would error. scripts/compose_smoke.sh tests exactly that: run a backtest,
down, and check the trial is still there.
Ownership. The image runs as a pinned non-root uid (1000) and creates each mount
point in the image so the named volumes inherit that ownership. Docker seeds a
volume's ownership from the image directory at that path and makes it root-owned
when nothing is there — which surfaces as "unable to open database file" the first
time anything journals. Because the uid is pinned, a host user can read the same
volume without sudo, which mixed native/container work against one journal
requires.
Connecting an MCP client
The server speaks stdio, so the mcp service is attach-oriented rather than a
listening port:
{"mcpServers": {"tradeflow": {"command": "docker",
"args": ["compose", "-f", "/path/to/tradeflow/docker-compose.yml", "run", "--rm", "mcp"]}}}
A TCP/SSE transport would add a port mapping and nothing else; it is not built.
Optional extras in the image
The image carries mcp, store, and viz by default. The research agent's ai
extra is opt-in, because the base image should need zero paid credentials:
docker compose build --build-arg EXTRAS="mcp,store,viz,ai"
Verifying the wiring
CI validates that the compose file parses (cheap, needs no daemon). The behavioral checks need Docker and are a script you run locally:
make compose-smoke
It checks that the config parses, that live is not in the default service set,
that the image's default command is safe, that the demo runs offline with no keys,
that state survives down, and that the volumes exist where the host can reach them.