Ctrl K
ComunidadGuías…

dev/compose.dev.yml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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