dev/compose.dev.yml
Rama main · réplica de lectura del repositorio original.
# Development compose with profiles
#
# Setup:
# cp .env.example .env # Add your keys
#
# **Use `make quickstart` — not direct compose.** ADR-004 (2026-05, #3860)
# established `make quickstart` as the single entry point for local dev. It
# sets VOLUME_PREFIX via dev/scripts/dev.sh so per-worktree volumes (goose_bin,
# api_modules, ui_modules, worker_modules, mcp_server_modules, see the volumes
# block)
# get isolated names; running
# `docker compose -f dev/compose.dev.yml up` directly collapses every worktree
# onto the same volumes.
#
# Recommended:
# make quickstart # interactive: asks what you're working on
# make quickstart frontend-only # no compose, fastest
# make quickstart backend-shared # postgres + redis + clickhouse + app
# make quickstart nlp # + NLP + langevals
# make quickstart full-local # everything
# make quickstart-help # full mode reference (non-interactive)
#
# Legacy targets `make dev`, `make dev-up`, `make dev-down`, `make dev-logs`
# still work for one release with a deprecation warning — they shim onto the
# new modes.
#
# Per-worktree ports (create .env.dev in your worktree):
# APP_PORT=5560 # the ui lane (Vite); the api lane is published on API_PORT
# API_PORT=6560
#
# Debug logging (set in shell or .env.dev):
# PINO_LOG_LEVEL=debug # For verbose worker/app logs
#
# Per-service memory limits are configured below on each service.
#
# AI Gateway (Go data plane) — NOT in this compose:
# The Go gateway runs on the host via `make service svc=aigateway`,
# binding :5563. Sourced from .env (same secrets as the app).
# Start it in a second terminal alongside any compose profile above.
#
# Stop everything:
# docker compose -f dev/compose.dev.yml --profile full down
x-common-env: &common-env
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
NODE_ENV: development
SKIP_TIKTOKEN: "true"
# Customer-egress policy is sourced from .env. Do not override it
# here: local/self-hosted setups may intentionally use private HTTP endpoints.
# NOTE: infrastructure URLs (DATABASE_URL, REDIS_URL, CLICKHOUSE_URL,
# LANGWATCH_NLP_SERVICE, LANGEVALS_ENDPOINT) are NOT set here. The
# contributor's `.env` is the source of truth — `quickstart`
# writes a per-mode overlay at `.env.dev-up` that overrides
# only the URLs whose services are starting locally for that mode
# (#3860 AC#2 / AC#6).
services:
# =============================================================================
# INFRASTRUCTURE (always started)
# =============================================================================
postgres:
image: postgres:16
restart: unless-stopped
environment:
POSTGRES_DB: mydb
POSTGRES_USER: prisma
POSTGRES_PASSWORD: prisma
# No host port - accessed via docker network as "postgres:5432"
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U prisma -d mydb"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 256m
cpus: "0.5"
# Singleton across worktrees (#3860 AC#5). Exposed on a fixed host port so
# parallel worktrees reuse the same container instead of spinning up their
# own. Compose treats this as belonging to the first project that brings it
# up; subsequent `quickstart`s detect the running container and reuse it.
#
# Prerequisite: no NON-Docker process (e.g. a host redis-server) may be
# holding 6379. `make quickstart` calls `check_host_redis_collision`
# (dev/scripts/dev.sh), which exempts this project's own already-running redis
# container (idempotent reuse) and only errors on a foreign listener it cannot
# rebind over. To free a host redis — macOS: `brew services stop redis`;
# Linux: `sudo systemctl stop redis-server`.
redis:
image: redis:alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
# Bulk-trace seeding piles 25–30k queue / event keys into
# Redis; the 64m ceiling was triggering an OOM kill during
# BGSAVE fork. 512m gives the dev stack headroom while still
# being a fraction of what prod would use.
memory: 512m
cpus: "0.25"
clickhouse:
image: langwatch/clickhouse-serverless:0.4.0
environment:
CLICKHOUSE_PASSWORD: langwatch
# No host port - accessed via docker network as "clickhouse:8123"
volumes:
- clickhouse-data:/var/lib/clickhouse
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8123/ping || exit 1"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
deploy:
resources:
limits:
# Eval-rich seed loads (1–4 evaluation_runs per trace, async
# inserts queued up via the customEvaluationSync reactor) were
# pushing CH past the 2g ceiling during BGSAVE / merges and
# the OvercommitTracker started killing inserts. 4g keeps the
# dev stack stable under the worst seed we've thrown at it.
memory: 4g
cpus: "1.0"
# =============================================================================
# INIT (installs dependencies once for Linux, all containers share)
# =============================================================================
init:
image: node:24.22.1
working_dir: /
volumes:
# The container mirrors the repo layout rooted at `/` (ADR-076: one pnpm
# workspace, one lockfile, at the root). The three applications are
# workspace members under /apps, so they mount at the paths the root
# `packages:` list uses, alongside the root manifest, lockfile, .npmrc and
# every other member. skills/ and dev/tests/agentic-e2e/ contribute
# manifests only — a frozen install checks the lockfile against every
# member it can see.
- ./apps:/apps
- ./pnpm-workspace.yaml:/pnpm-workspace.yaml:ro
- ./pnpm-lock.yaml:/pnpm-lock.yaml:ro
- ./package.json:/package.json:ro
- ./.npmrc:/.npmrc:ro
- ./packages:/packages
- ./mcp/typescript:/mcp/typescript
- mcp_server_modules:/mcp/typescript/node_modules
- ./skills/package.json:/skills/package.json:ro
- ./dev/tests/agentic-e2e/package.json:/dev/tests/agentic-e2e/package.json:ro
- ./services/langevals:/services/langevals
- ./sdks/typescript:/sdks/typescript:ro
- ./sdks/python:/sdks/python:ro
# node_modules overlays: the STORE lives at the workspace root
# (/node_modules/.pnpm) and each application's own node_modules holds only
# symlinks into it. All stay in named volumes so the container's Linux
# install never writes into the host's macOS tree (ADR-004).
- root_modules:/node_modules
- api_modules:/apps/api/node_modules
- ui_modules:/apps/ui/node_modules
- worker_modules:/apps/worker/node_modules
- tasks_modules:/apps/tasks/node_modules
- pnpm_store:/root/.local/share/pnpm/store
- goose_bin:/opt/goose-bin
command: >
sh -c "
corepack enable &&
pnpm config set store-dir /root/.local/share/pnpm/store &&
mkdir -p /node_modules /apps/api/node_modules /apps/ui/node_modules /apps/worker/node_modules /apps/tasks/node_modules &&
GOOSE_VERSION=v3.26.0 &&
ARCH=$$(uname -m) &&
if [ \"$$ARCH\" = \"aarch64\" ]; then GOOSE_ARCH=arm64; else GOOSE_ARCH=x86_64; fi &&
GOOSE_MARKER=\"$$GOOSE_VERSION/$$GOOSE_ARCH\" &&
if [ ! -x /opt/goose-bin/goose ] || [ \"$$(cat /opt/goose-bin/.goose-meta 2>/dev/null)\" != \"$$GOOSE_MARKER\" ]; then
echo \"Installing goose $$GOOSE_VERSION ($$GOOSE_ARCH) for ClickHouse migrations...\" &&
curl -fsSL \"https://github.com/pressly/goose/releases/download/$${GOOSE_VERSION}/goose_linux_$${GOOSE_ARCH}\" -o /opt/goose-bin/goose &&
chmod +x /opt/goose-bin/goose &&
printf '%s' \"$$GOOSE_MARKER\" > /opt/goose-bin/.goose-meta;
fi &&
pnpm install --frozen-lockfile --filter '@langwatch/platform-api...' --filter '@langwatch/ui...' --filter '@langwatch/worker...' --filter '@langwatch/tasks...' &&
pnpm --filter @langwatch/mcp-server run build &&
pnpm --filter @langwatch/prisma-client run prisma:generate
"
environment:
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
CI: "true"
# =============================================================================
# UI (the browser application — mounted for hot reload)
# =============================================================================
# Vite serves the SPA and proxies /api to the api service, so the browser
# still talks to ONE origin. The api service is published too, for a caller
# that wants the API without the proxy hop.
ui:
image: node:24.22.1
restart: unless-stopped
working_dir: /apps/ui
volumes:
# See the init service for why the layout is rooted at `/` (ADR-076).
- ./apps:/apps
- ./pnpm-workspace.yaml:/pnpm-workspace.yaml:ro
- ./pnpm-lock.yaml:/pnpm-lock.yaml:ro
- ./package.json:/package.json:ro
- ./.npmrc:/.npmrc:ro
- ./packages:/packages
- ./mcp/typescript:/mcp/typescript
- mcp_server_modules:/mcp/typescript/node_modules
- ./skills/package.json:/skills/package.json:ro
- ./dev/tests/agentic-e2e/package.json:/dev/tests/agentic-e2e/package.json:ro
- ./services/langevals:/services/langevals
- ./sdks/typescript:/sdks/typescript:ro
- ./sdks/python:/sdks/python:ro
- root_modules:/node_modules
- api_modules:/apps/api/node_modules
- ui_modules:/apps/ui/node_modules
- worker_modules:/apps/worker/node_modules
command: >
sh -c "
corepack enable &&
pnpm run dev
"
ports:
- "${APP_PORT:-5560}:5560"
environment:
<<: *common-env
# BASE_HOST/NEXTAUTH_URL intentionally override .env so
# worktree-isolated dev gets the correct dynamic APP_PORT (each
# worktree picks a different free port). Set shell vars to point at
# a proxied URL (e.g. BASE_HOST=https://langwatch-main.example.com) —
# shell wins over the localhost fallback.
BASE_HOST: ${BASE_HOST:-http://localhost:${APP_PORT:-5560}}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:${APP_PORT:-5560}}
# Enable scenarios UI in dev
RELEASE_UI_SIMULATIONS_MENU_ENABLED: "1"
RELEASE_UI_SUITES_ENABLED: "1"
# Use email login for dev (no Auth0 dependency)
AUTH_PROVIDER: "email"
# Where Vite proxies /api. On the host the default is PORT+1000 on
# loopback; in compose the API is a sibling service, so it is named.
LANGWATCH_API_URL: http://api:6560
env_file:
- .env
- path: .env.dev-up
required: false
depends_on:
init:
condition: service_completed_successfully
api:
condition: service_started
tty: true
stdin_open: true
deploy:
resources:
limits:
memory: 2g
cpus: "1.0"
# =============================================================================
# API (tRPC + REST + SSE — mounted for hot reload)
# =============================================================================
# The upgrade runs here (`start:prepare:db`: `pnpm task upgrade`, then the
# system-migrations pass), once, before the process starts. The ui lane waits on this service, so a browser never loads the SPA
# against a half-migrated database.
api:
image: node:24.22.1
restart: unless-stopped
working_dir: /apps/api
volumes:
# See the init service for why the layout is rooted at `/` (ADR-076).
- ./apps:/apps
- ./pnpm-workspace.yaml:/pnpm-workspace.yaml:ro
- ./pnpm-lock.yaml:/pnpm-lock.yaml:ro
- ./package.json:/package.json:ro
- ./.npmrc:/.npmrc:ro
- ./packages:/packages
- ./mcp/typescript:/mcp/typescript
- mcp_server_modules:/mcp/typescript/node_modules
- ./skills/package.json:/skills/package.json:ro
- ./dev/tests/agentic-e2e/package.json:/dev/tests/agentic-e2e/package.json:ro
- ./services/langevals:/services/langevals
- ./sdks/typescript:/sdks/typescript:ro
- ./sdks/python:/sdks/python:ro
- root_modules:/node_modules
- api_modules:/apps/api/node_modules
- ui_modules:/apps/ui/node_modules
- worker_modules:/apps/worker/node_modules
- tasks_modules:/apps/tasks/node_modules
- goose_bin:/opt/goose-bin:ro
command: >
sh -c "
export PATH=/opt/goose-bin:$$PATH &&
corepack enable &&
pnpm --silent run start:prepare:db &&
pnpm run dev
"
ports:
- "${API_PORT:-6560}:6560"
environment:
<<: *common-env
API_PORT: "6560"
# The browser reaches the API through the ui lane's origin, so the public
# base is still the ui port; only the listener above is this service's own.
BASE_HOST: ${BASE_HOST:-http://localhost:${APP_PORT:-5560}}
NEXTAUTH_URL: ${NEXTAUTH_URL:-http://localhost:${APP_PORT:-5560}}
RELEASE_UI_SIMULATIONS_MENU_ENABLED: "1"
RELEASE_UI_SUITES_ENABLED: "1"
AUTH_PROVIDER: "email"
env_file:
- .env
- path: .env.dev-up
required: false
depends_on:
init:
condition: service_completed_successfully
postgres:
condition: service_healthy
redis:
condition: service_healthy
clickhouse:
condition: service_healthy
tty: true
stdin_open: true
deploy:
resources:
limits:
memory: 4g
cpus: "2.0"
# =============================================================================
# WORKERS (profile: workers)
# =============================================================================
# Started by quickstart presets: all-local, all-local-nlp, dev-storage, dev-infra, full-local.
# Skipped by: frontend-only, which runs `pnpm dev` on the host — and that runs
# the worker as one of its three processes, so nothing is lost there either.
# migration is a short-lived host-port stack for prisma/clickhouse migrate;
# it never runs an application, so no workers.
workers:
image: node:24.22.1
working_dir: /apps/worker
profiles: [workers, scenarios, full]
volumes:
# See the init service for why the layout is rooted at `/` (ADR-076).
- ./apps:/apps
- ./pnpm-workspace.yaml:/pnpm-workspace.yaml:ro
- ./pnpm-lock.yaml:/pnpm-lock.yaml:ro
- ./package.json:/package.json:ro
- ./.npmrc:/.npmrc:ro
- ./packages:/packages
- ./mcp/typescript:/mcp/typescript
- mcp_server_modules:/mcp/typescript/node_modules
- ./skills/package.json:/skills/package.json:ro
- ./dev/tests/agentic-e2e/package.json:/dev/tests/agentic-e2e/package.json:ro
- ./services/langevals:/services/langevals
- ./sdks/typescript:/sdks/typescript:ro
- ./sdks/python:/sdks/python:ro
- root_modules:/node_modules
- api_modules:/apps/api/node_modules
- ui_modules:/apps/ui/node_modules
- worker_modules:/apps/worker/node_modules
- goose_bin:/opt/goose-bin:ro
command: sh -c "export PATH=/opt/goose-bin:$$PATH && corepack enable && while true; do node --experimental-transform-types src/main.ts; sleep 1; done"
environment:
<<: *common-env
# Override BASE_HOST for Docker networking - scenario child processes post
# events here. It names the ui lane, not the api lane: /api is served under
# the ui origin (Vite proxies it), so this stays the one address a caller
# needs, exactly as it was when a single process served both.
BASE_HOST: http://ui:5560
LANGWATCH_ENDPOINT: http://ui:5560
# Override REDIS_URL: host app uses localhost:6379, workers must use compose service name
REDIS_URL: redis://redis:6379
PINO_LOG_LEVEL: ${PINO_LOG_LEVEL:-info}
# To enable verbose scenario SDK logging, add: SCENARIO_VERBOSE: "true"
env_file:
- .env
- path: .env.dev-up
required: false
depends_on:
init:
condition: service_completed_successfully
deploy:
resources:
limits:
memory: 1536m
cpus: "0.5"
# =============================================================================
# NLP SERVICE (profile: nlp)
# =============================================================================
# nlpgo — the Go NLP engine (services/nlpgo, run as the `nlpgo` subcommand of
# the cmd/service monobinary), the nlp target of Dockerfile.go-services.
# There is no Python uvicorn/litellm service anymore. The image still
# publishes as langwatch/langwatch_nlp and binds :5561, so the app/workers
# dial http://langwatch_nlp:5561 (LANGWATCH_NLP_SERVICE) unchanged.
langwatch_nlp:
# Build from local services/nlpgo source (the nlp target of Dockerfile.go-services)
# instead of the published langwatch/langwatch_nlp:latest image, which
# lags the source. Building from source keeps the engine in lockstep with
# this checkout, so evaluator output (e.g. the `details`/reasoning
# envelope) matches the code here. See #5151.
build:
context: .
dockerfile: infra/docker/Dockerfile.go-services
target: nlp
command: [nlpgo]
profiles: [nlp, scenarios, full]
# No host port - accessed via docker network as "langwatch_nlp:5561"
environment:
# nlpgo reads LANGWATCH_ENDPOINT for evaluator + agent-workflow callbacks
# (falls back to it when NLPGO_ENGINE_LANGWATCH_BASE_URL is unset).
LANGWATCH_ENDPOINT: http://app:5560
SERVER_ADDR: ":5561"
deploy:
resources:
limits:
memory: 1024m
cpus: "0.5"
langevals:
# Build from local ./langevals source (via Dockerfile.langevals) instead
# of the published langwatch/langevals:latest image, which lags the
# source. Newer evaluators present in this checkout 404 against :latest;
# building from source guarantees the container serves exactly the
# evaluators in this checkout. See #5151.
build:
context: .
dockerfile: infra/docker/Dockerfile.langevals
profiles: [nlp, full]
# No host port - accessed via docker network as "langevals:5562"
environment:
DISABLE_EVALUATORS_PRELOAD: "true"
deploy:
resources:
limits:
# langevals imports ragas/langchain at boot and OOM-kills at 256m
# before it can serve a single request — even with preload disabled.
# Production (charts/langwatch) requests 6Gi/limits 8Gi; 4g is enough
# for the dev import footprint plus LLM-judge evaluators.
memory: 4g
cpus: "0.5"
# =============================================================================
# TEST TOOLS (profile: test)
# =============================================================================
# The `ai-server` mock-provider service is GONE, and there is no replacement
# here: its only implementation was a script in the platform application, which
# the platform delete takes with it. The `test` profile is empty until the
# tool lands somewhere that survives — a service pointing at a script nobody
# ships is a slower way to discover the same absence. `AI_SERVER_PORT` is no
# longer read by anything in this file.
# The local observability stack (grafana/otel-lgtm) is NOT a compose service.
# haven owns it: it runs the bundle on colima with explicit memory/CPU/pids/log
# and retention caps, and wires every stack to it automatically. Bring it up
# with `make observability` (or `haven observability up`).
# See dev/docs/adr/042-local-observability-stack.md.
volumes:
# Stateful — shared across worktrees (#3860 AC#4 / AC#5).
# Sign-up persists when switching worktrees; ClickHouse traces survive too.
# Two worktrees can't have the same stateful container `up` simultaneously
# (postgres locks the volume) — `dev/scripts/dev.sh` detects and warns.
db-data:
name: langwatch-db-data
redis-data:
name: langwatch-redis-data
clickhouse-data:
name: langwatch-clickhouse-data
# Per-worktree — Linux-vs-host platform isolation per ADR-004.
# node_modules differs across branches with different lockfiles, so these
# stay namespaced.
goose_bin:
name: ${VOLUME_PREFIX:-langwatch}-goose-bin
api_modules:
name: ${VOLUME_PREFIX:-langwatch}-api-modules
ui_modules:
name: ${VOLUME_PREFIX:-langwatch}-ui-modules
worker_modules:
name: ${VOLUME_PREFIX:-langwatch}-worker-modules
tasks_modules:
name: ${VOLUME_PREFIX:-langwatch}-tasks-modules
# The workspace-root store (/node_modules/.pnpm). Since ADR-076 the install
# root is the repo root, so this is where every real package body lives — each
# application's node_modules (above) holds only symlinks into it.
root_modules:
name: ${VOLUME_PREFIX:-langwatch}-root-modules
# mcp-server's install + build run in the `init` container against the
# `./mcp/typescript` bind mount. Without isolation the container install writes
# container-shaped pnpm symlinks into the host tree, which then dangle and
# break a host-side build (#5144). It is a plain workspace member since
# ADR-076 (its store rides in root_modules), but its own node_modules links
# stay isolated for the same reason.
mcp_server_modules:
name: ${VOLUME_PREFIX:-langwatch}-mcp-server-modules
# Always shared (cache only).
pnpm_store:
name: langwatch-pnpm-store