TurfAITurfAI User Guide
ReferenceOperations

TurfAI Deployment Strategy & Implementation Guide

Synced from the TurfAI source on 2026-06-21.

Document Version: 1.0 Date: 2025-11-01 Status: Implementation Ready


Executive Summary

This guide provides a comprehensive deployment strategy for TurfAI's microservices architecture, focusing on:

  1. Local Development Environment - Easy start/stop without Docker
  2. Database Initialization Automation - Zero manual setup
  3. Docker Compose - Production-like testing
  4. Cloud Run Deployment - Production deployment
  5. Logging & Tracing - Solve distributed logging challenges

Architecture Overview

Services

  1. DMS (Strapi/Node.js) - Document management + API gateway - Port 1337
  2. LLM Service (FastAPI/Python) - AI processing - Port 8080
  3. Router (FastAPI/Python) - Job routing - Port 9001
  4. Processor (Python) - Document processing workers
  5. RAG Processor (Python) - Embedding generation
  6. RAG Query Service (FastAPI/Python) - Query engine - Port 8003

Dependencies

  1. PostgreSQL (Port 5432) - Main database for DMS
  2. PostgreSQL with pgvector (Port 5433) - RAG embeddings
  3. Redis (Port 6379) - Job queue
  4. GCS - File storage

Frontend

React app pointing ONLY to DMS URL. DMS handles all CORS and proxies to internal services.


Phase 1: Local Development Environment (Day 1)

Goal: Easy start/stop without Docker for rapid development

Solution: PM2 Process Manager

Why PM2:

  • ✅ Built-in log aggregation
  • ✅ Auto-restart on crash
  • ✅ Status dashboard (pm2 monit)
  • ✅ Single command start/stop
  • ✅ Per-service or unified logs

Implementation

1. Install PM2

npm install -g pm2

2. Create Directory Structure

mkdir -p dev/scripts

3. Create PM2 Configuration

File: dev/ecosystem.config.js

module.exports = {
  apps: [
    // DMS - Strapi CMS (Port 1337)
    {
      name: 'dms',
      cwd: './dms',
      script: 'npm',
      args: 'run develop',
      env: {
        PORT: 1337,
        NODE_ENV: 'development',
        DATABASE_HOST: 'localhost',
        DATABASE_PORT: 5432,
        DATABASE_NAME: 'turfai_dms',
        DATABASE_USERNAME: 'postgres',
        DATABASE_PASSWORD: 'postgres',
        GCS_BUCKET_NAME: process.env.GCS_BUCKET_NAME
      },
      watch: false,
      instances: 1,
      autorestart: true,
      max_memory_restart: '1G'
    },

    // LLM Service - AI Processing (Port 8080)
    {
      name: 'llm-service',
      cwd: './llm-service/api',
      script: 'uvicorn',
      args: 'main:app --reload --host 0.0.0.0 --port 8080',
      interpreter: 'python3',
      env: {
        OPENAI_API_KEY: process.env.OPENAI_API_KEY,
        ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
        GOOGLE_CLOUD_PROJECT: process.env.GOOGLE_CLOUD_PROJECT,
        VERTEX_LOCATION: process.env.VERTEX_LOCATION || 'us-central1'
      },
      watch: false,
      instances: 1,
      autorestart: true,
      max_memory_restart: '2G'
    },

    // Router Service - Job Routing (Port 9001)
    {
      name: 'router',
      cwd: './router',
      script: 'uvicorn',
      args: 'main:app --reload --host 0.0.0.0 --port 9001',
      interpreter: 'python3',
      env: {
        REDIS_HOST: 'localhost',
        REDIS_PORT: 6379,
        DMS_URL: 'http://localhost:1337',
        ROUTER_API_KEY: process.env.ROUTER_API_KEY || '6f2452d7-c1c1-422e-9cb6-e958d560e06b'
      },
      watch: false,
      instances: 1,
      autorestart: true,
      max_memory_restart: '512M'
    },

    // RAG Query Service - Query Engine (Port 8003)
    {
      name: 'rag-query',
      cwd: './rag_query_service',
      script: 'uvicorn',
      args: 'main:app --reload --host 0.0.0.0 --port 8003',
      interpreter: 'python3',
      env: {
        POSTGRES_HOST: 'localhost',
        POSTGRES_PORT: 5433,
        POSTGRES_DB: 'turfai_rag',
        POSTGRES_USER: 'postgres',
        POSTGRES_PASSWORD: 'postgres',
        DMS_URL: 'http://localhost:1337',
        LLM_SERVICE_URL: 'http://localhost:8080'
      },
      watch: false,
      instances: 1,
      autorestart: true,
      max_memory_restart: '1G'
    },

    // Processor - Document Processing Worker
    {
      name: 'processor',
      cwd: './processors',
      script: 'main.py',
      interpreter: 'python3',
      env: {
        REDIS_HOST: 'localhost',
        REDIS_PORT: 6379,
        DMS_URL: 'http://localhost:1337',
        LLM_SERVICE_URL: 'http://localhost:8080',
        RAG_PROCESSOR_URL: 'http://localhost:8002'
      },
      watch: false,
      instances: 1,
      autorestart: true,
      max_memory_restart: '1G'
    }
  ]
};

4. Create Start Script

File: dev/scripts/start-local.sh

#!/bin/bash
set -e

echo "🚀 Starting TurfAI Local Development Environment..."
echo ""

# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color

# Check if .env.local exists
if [ ! -f .env.local ]; then
    echo -e "${RED}❌ .env.local not found!${NC}"
    echo "Please copy .env.example to .env.local and configure it"
    exit 1
fi

# Load environment variables
export $(grep -v '^#' .env.local | xargs)

# Check dependencies
echo -e "${YELLOW}📋 Checking dependencies...${NC}"

# Check PostgreSQL
if ! command -v psql &> /dev/null; then
    echo -e "${RED}❌ PostgreSQL not found. Please install: brew install postgresql@15${NC}"
    exit 1
fi

# Check Redis
if ! command -v redis-cli &> /dev/null; then
    echo -e "${RED}❌ Redis not found. Please install: brew install redis${NC}"
    exit 1
fi

# Check PM2
if ! command -v pm2 &> /dev/null; then
    echo -e "${RED}❌ PM2 not found. Installing...${NC}"
    npm install -g pm2
fi

# Check Python
if ! command -v python3 &> /dev/null; then
    echo -e "${RED}❌ Python3 not found. Please install Python 3.8+${NC}"
    exit 1
fi

# Check Node.js
if ! command -v node &> /dev/null; then
    echo -e "${RED}❌ Node.js not found. Please install Node.js 16+${NC}"
    exit 1
fi

echo -e "${GREEN}✅ All dependencies found${NC}"
echo ""

# Start PostgreSQL if not running
echo -e "${YELLOW}🐘 Starting PostgreSQL...${NC}"
if ! pg_isready -q; then
    brew services start postgresql@15
    sleep 2
fi
echo -e "${GREEN}✅ PostgreSQL running${NC}"

# Start Redis if not running
echo -e "${YELLOW}📦 Starting Redis...${NC}"
if ! redis-cli ping &> /dev/null; then
    brew services start redis
    sleep 2
fi
echo -e "${GREEN}✅ Redis running${NC}"

# Initialize databases (first time only)
if [ ! -f .db-initialized ]; then
    echo -e "${YELLOW}🗄️  Initializing databases...${NC}"
    ./dev/scripts/init-db.sh
    touch .db-initialized
    echo -e "${GREEN}✅ Databases initialized${NC}"
else
    echo -e "${GREEN}✅ Databases already initialized${NC}"
fi

echo ""
echo -e "${YELLOW}🚀 Starting all services with PM2...${NC}"

# Start all services
pm2 start dev/ecosystem.config.js

echo ""
echo -e "${GREEN}✅ All services started successfully!${NC}"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "📊 Service Status:"
echo "  • DMS:              http://localhost:1337"
echo "  • LLM Service:      http://localhost:8080"
echo "  • Router:           http://localhost:9001"
echo "  • RAG Query:        http://localhost:8003"
echo "  • Processor:        Running in background"
echo ""
echo "📋 Useful Commands:"
echo "  • View logs:        pm2 logs"
echo "  • View logs (one):  pm2 logs dms"
echo "  • Monitor:          pm2 monit"
echo "  • Status:           pm2 status"
echo "  • Stop all:         ./dev/scripts/stop-local.sh"
echo "  • Restart one:      pm2 restart dms"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
chmod +x dev/scripts/start-local.sh

5. Create Stop Script

File: dev/scripts/stop-local.sh

#!/bin/bash
set -e

echo "🛑 Stopping TurfAI services..."

pm2 delete all

echo "✅ All services stopped"
echo ""
echo "Note: PostgreSQL and Redis are still running"
echo "To stop them:"
echo "  brew services stop postgresql@15"
echo "  brew services stop redis"
chmod +x dev/scripts/stop-local.sh

6. Create Logs Script

File: dev/scripts/logs.sh

#!/bin/bash

# View logs for specific service or all
if [ -z "$1" ]; then
    echo "📊 Viewing logs for ALL services (Ctrl+C to exit)"
    pm2 logs
else
    echo "📊 Viewing logs for: $1"
    pm2 logs "$1"
fi
chmod +x dev/scripts/logs.sh

Phase 2: Database Initialization Automation (Day 1)

Goal: Zero manual database setup

File: dev/scripts/init-db.sh

#!/bin/bash
set -e

echo "📦 Initializing TurfAI Databases..."
echo ""

# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'

# 1. Create main database for DMS
echo -e "${YELLOW}Creating turfai_dms database...${NC}"
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'turfai_dms'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE turfai_dms"
echo -e "${GREEN}✅ turfai_dms database ready${NC}"

# 2. Create RAG database with pgvector
echo -e "${YELLOW}Creating turfai_rag database with pgvector...${NC}"
psql -U postgres -tc "SELECT 1 FROM pg_database WHERE datname = 'turfai_rag'" | grep -q 1 || psql -U postgres -c "CREATE DATABASE turfai_rag"
psql -U postgres -d turfai_rag -c "CREATE EXTENSION IF NOT EXISTS vector"
echo -e "${GREEN}✅ turfai_rag database ready with pgvector${NC}"

# 3. Run RAG schema
echo -e "${YELLOW}Creating RAG tables...${NC}"
if [ -f rag_query_service/schema.sql ]; then
    psql -U postgres -d turfai_rag -f rag_query_service/schema.sql
    echo -e "${GREEN}✅ RAG tables created${NC}"
else
    echo -e "${YELLOW}⚠️  RAG schema.sql not found, skipping...${NC}"
fi

# 4. DMS will auto-migrate on first start
echo -e "${GREEN}✅ DMS will auto-migrate on first start${NC}"

echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN}✅ Database initialization complete!${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "Next steps:"
echo "1. Start services: ./dev/scripts/start-local.sh"
echo "2. Create Strapi admin user: http://localhost:1337/admin"
chmod +x dev/scripts/init-db.sh

Phase 3: Logging Solution - Correlation IDs (Day 1-2)

The Problem

With 6 microservices, tracing a single request across services is difficult:

  • Request starts in DMS
  • Goes to Router
  • Router sends to LLM Service
  • LLM Service may call RAG Query
  • RAG Query calls back to DMS

Solution: Correlation IDs + Structured Logging

Implementation

1. DMS - Generate Correlation ID

File: dms/src/middlewares/correlation-id.js

const { v4: uuidv4 } = require('uuid');

module.exports = (config, { strapi }) => {
  return async (ctx, next) => {
    // Get correlation ID from header or generate new one
    const correlationId = ctx.request.header['x-correlation-id'] || uuidv4();

    // Store in context
    ctx.correlationId = correlationId;

    // Add to response headers
    ctx.set('x-correlation-id', correlationId);

    // Add to Strapi logger
    strapi.log.info(`[${correlationId}] ${ctx.method} ${ctx.url}`);

    await next();
  };
};

Register middleware in dms/config/middlewares.js:

module.exports = [
  'strapi::logger',
  'strapi::errors',
  'strapi::security',
  'global::correlation-id',  // ADD THIS
  'strapi::cors',
  // ... rest
];

2. DMS - Forward Correlation ID to Services

In DMS API calls:

// Example: dms/src/api/rag/controllers/rag.js
async query(ctx) {
  const correlationId = ctx.correlationId;

  const response = await axios.post(
    `${ragQueryUrl}/api/v1/rag/query`,
    ragRequest,
    {
      headers: {
        'Authorization': authHeader,
        'Content-Type': 'application/json',
        'x-correlation-id': correlationId  // PASS IT ALONG
      }
    }
  );

  strapi.log.info(`[${correlationId}] RAG query completed`);
  return ctx.send(response.data);
}

3. Python Services - Extract and Log Correlation ID

Create utility: processors/utils/correlation_logger.py

import logging
from contextvars import ContextVar
from typing import Optional

# Context variable to store correlation ID
correlation_id_var: ContextVar[Optional[str]] = ContextVar('correlation_id', default=None)

class CorrelationFilter(logging.Filter):
    """Add correlation ID to all log records."""

    def filter(self, record):
        correlation_id = correlation_id_var.get()
        record.correlation_id = correlation_id or 'NO_CORRELATION_ID'
        return True

def setup_correlation_logging():
    """Setup logging with correlation ID support."""

    # Create formatter with correlation ID
    formatter = logging.Formatter(
        '%(asctime)s - [%(correlation_id)s] - %(name)s - %(levelname)s - %(message)s'
    )

    # Get root logger
    logger = logging.getLogger()

    # Add correlation filter to all handlers
    for handler in logger.handlers:
        handler.addFilter(CorrelationFilter())
        handler.setFormatter(formatter)

def set_correlation_id(correlation_id: str):
    """Set correlation ID for current context."""
    correlation_id_var.set(correlation_id)

def get_correlation_id() -> Optional[str]:
    """Get correlation ID from current context."""
    return correlation_id_var.get()

4. FastAPI Services - Middleware for Correlation ID

File: llm-service/api/main.py (and similar for router, rag_query_service)

from fastapi import FastAPI, Request
from processors.utils.correlation_logger import (
    setup_correlation_logging,
    set_correlation_id,
    get_correlation_id
)
import uuid
import logging

app = FastAPI()

# Setup correlation logging
setup_correlation_logging()
logger = logging.getLogger(__name__)

@app.middleware("http")
async def correlation_id_middleware(request: Request, call_next):
    # Extract correlation ID from header or generate new one
    correlation_id = request.headers.get('x-correlation-id', str(uuid.uuid4()))

    # Set in context
    set_correlation_id(correlation_id)

    # Log incoming request
    logger.info(f"Incoming request: {request.method} {request.url.path}")

    # Process request
    response = await call_next(request)

    # Add correlation ID to response headers
    response.headers['x-correlation-id'] = correlation_id

    return response

5. Example: Complete Request Flow with Correlation

User Request → DMS
[abc-123] DMS: POST /api/rag/query

[abc-123] DMS: Forwarding to RAG Query Service

[abc-123] RAG Query: Received query request

[abc-123] RAG Query: Calling LLM Service

[abc-123] LLM Service: Processing chat request

[abc-123] LLM Service: Response generated

[abc-123] RAG Query: Query completed

[abc-123] DMS: Returning response to client

6. Viewing Correlated Logs

Local Development (PM2):

# View all logs
pm2 logs

# Search for specific correlation ID
pm2 logs | grep "abc-123"

# Or use jq for JSON logs
pm2 logs --json | jq 'select(.correlation_id == "abc-123")'

Better: Use lnav (Log File Navigator):

brew install lnav

# View PM2 logs with filtering
pm2 logs --raw > /tmp/turfai.log &
lnav /tmp/turfai.log

# In lnav, press '/' to search for correlation ID

Phase 4: Docker Compose for Testing (Day 2)

Goal: Production-like environment for integration testing

File: docker-compose.yml

version: '3.8'

services:
  # PostgreSQL for DMS
  postgres:
    image: postgres:15
    container_name: turfai-postgres
    environment:
      POSTGRES_DB: turfai_dms
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - turfai-network

  # PostgreSQL with pgvector for RAG
  postgres-rag:
    image: pgvector/pgvector:pg15
    container_name: turfai-postgres-rag
    environment:
      POSTGRES_DB: turfai_rag
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_rag_data:/var/lib/postgresql/data
      - ./rag_query_service/schema.sql:/docker-entrypoint-initdb.d/schema.sql
    ports:
      - "5433:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
    networks:
      - turfai-network

  # Redis for job queue
  redis:
    image: redis:7-alpine
    container_name: turfai-redis
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - turfai-network

  # DMS - Strapi CMS
  dms:
    build:
      context: ./dms
      dockerfile: Dockerfile
    container_name: turfai-dms
    ports:
      - "1337:1337"
    environment:
      DATABASE_HOST: postgres
      DATABASE_PORT: 5432
      DATABASE_NAME: turfai_dms
      DATABASE_USERNAME: postgres
      DATABASE_PASSWORD: postgres
      DATABASE_SSL: "false"
      GCS_BUCKET_NAME: ${GCS_BUCKET_NAME}
      GOOGLE_APPLICATION_CREDENTIALS: /app/gcp-credentials.json
    depends_on:
      postgres:
        condition: service_healthy
    volumes:
      - ./dms:/app
      - /app/node_modules
      - ${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json:ro
    networks:
      - turfai-network
    labels:
      - "service=dms"

  # LLM Service
  llm-service:
    build:
      context: ./llm-service
      dockerfile: api/Dockerfile
    container_name: turfai-llm-service
    ports:
      - "8080:8080"
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY}
      GOOGLE_CLOUD_PROJECT: ${GOOGLE_CLOUD_PROJECT}
      VERTEX_LOCATION: ${VERTEX_LOCATION:-us-central1}
      GOOGLE_APPLICATION_CREDENTIALS: /app/gcp-credentials.json
    volumes:
      - ./llm-service:/app
      - ${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json:ro
    networks:
      - turfai-network
    labels:
      - "service=llm-service"

  # Router Service
  router:
    build:
      context: ./router
      dockerfile: Dockerfile
    container_name: turfai-router
    ports:
      - "9001:9001"
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
      DMS_URL: http://dms:1337
      ROUTER_API_KEY: ${ROUTER_API_KEY:-6f2452d7-c1c1-422e-9cb6-e958d560e06b}
    depends_on:
      redis:
        condition: service_healthy
      dms:
        condition: service_started
    volumes:
      - ./router:/app
    networks:
      - turfai-network
    labels:
      - "service=router"

  # RAG Query Service
  rag-query:
    build:
      context: ./rag_query_service
      dockerfile: Dockerfile
    container_name: turfai-rag-query
    ports:
      - "8003:8003"
    environment:
      POSTGRES_HOST: postgres-rag
      POSTGRES_PORT: 5432
      POSTGRES_DB: turfai_rag
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      DMS_URL: http://dms:1337
      LLM_SERVICE_URL: http://llm-service:8080
    depends_on:
      postgres-rag:
        condition: service_healthy
      llm-service:
        condition: service_started
    volumes:
      - ./rag_query_service:/app
    networks:
      - turfai-network
    labels:
      - "service=rag-query"

  # Processor Service
  processor:
    build:
      context: ./processors
      dockerfile: Dockerfile
    container_name: turfai-processor
    environment:
      REDIS_HOST: redis
      REDIS_PORT: 6379
      DMS_URL: http://dms:1337
      LLM_SERVICE_URL: http://llm-service:8080
      GOOGLE_APPLICATION_CREDENTIALS: /app/gcp-credentials.json
    depends_on:
      redis:
        condition: service_healthy
      dms:
        condition: service_started
    volumes:
      - ./processors:/app
      - ${GOOGLE_APPLICATION_CREDENTIALS}:/app/gcp-credentials.json:ro
    networks:
      - turfai-network
    labels:
      - "service=processor"

volumes:
  postgres_data:
  postgres_rag_data:

networks:
  turfai-network:
    driver: bridge

Create .env file for Docker Compose:

# Copy example
cp .env.example .env

# Edit with your values
GOOGLE_CLOUD_PROJECT=your-project
VERTEX_LOCATION=us-central1
GCS_BUCKET_NAME=your-bucket
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GOOGLE_APPLICATION_CREDENTIALS=/path/to/credentials.json
ROUTER_API_KEY=6f2452d7-c1c1-422e-9cb6-e958d560e06b

Docker Compose Commands:

# Start all services
docker-compose up -d

# View logs (all services)
docker-compose logs -f

# View logs (specific service)
docker-compose logs -f dms

# View logs with correlation ID
docker-compose logs -f | grep "abc-123"

# Check service status
docker-compose ps

# Restart specific service
docker-compose restart dms

# Stop all services
docker-compose down

# Stop and remove volumes (fresh start)
docker-compose down -v

# Rebuild after code changes
docker-compose up -d --build dms

Phase 5: Cloud Run Deployment (Day 3-5)

Architecture

Internet

Frontend (Vercel/Netlify)
    ↓ HTTPS
Cloud Load Balancer

DMS (Cloud Run - Public)

Internal Services (Cloud Run - Private Ingress):
    ├─ LLM Service
    ├─ Router
    ├─ RAG Query Service
    └─ Processor (Cloud Run Jobs)

Managed Services:
    ├─ Cloud SQL (PostgreSQL + pgvector)
    ├─ Memorystore (Redis)
    └─ Cloud Storage (GCS)

Prerequisites

# Install Google Cloud SDK
brew install --cask google-cloud-sdk

# Authenticate
gcloud auth login

# Set project
gcloud config set project YOUR_PROJECT_ID

# Enable APIs
gcloud services enable \
  run.googleapis.com \
  sqladmin.googleapis.com \
  redis.googleapis.com \
  cloudbuild.googleapis.com \
  secretmanager.googleapis.com

Setup Cloud SQL

# Create PostgreSQL instance for DMS
gcloud sql instances create turfai-postgres \
  --database-version=POSTGRES_15 \
  --tier=db-g1-small \
  --region=us-central1 \
  --root-password=CHANGE_ME

# Create database
gcloud sql databases create turfai_dms \
  --instance=turfai-postgres

# Create PostgreSQL instance for RAG (with more resources)
gcloud sql instances create turfai-postgres-rag \
  --database-version=POSTGRES_15 \
  --tier=db-custom-2-8192 \
  --region=us-central1 \
  --root-password=CHANGE_ME

# Create database
gcloud sql databases create turfai_rag \
  --instance=turfai-postgres-rag

# Install pgvector extension (manual step via Cloud SQL proxy)
gcloud sql connect turfai-postgres-rag --user=postgres
# Then: CREATE EXTENSION vector;

Setup Redis (Memorystore)

gcloud redis instances create turfai-redis \
  --size=1 \
  --region=us-central1 \
  --redis-version=redis_7_0

Setup Secrets

# Store API keys in Secret Manager
echo -n "sk-your-openai-key" | gcloud secrets create openai-api-key --data-file=-
echo -n "sk-ant-your-anthropic-key" | gcloud secrets create anthropic-api-key --data-file=-
echo -n "your-router-api-key" | gcloud secrets create router-api-key --data-file=-

# GCP service account key (if needed)
gcloud secrets create gcp-credentials --data-file=/path/to/credentials.json

Deployment Script

File: dev/scripts/deploy-cloud-run.sh

#!/bin/bash
set -e

# Configuration
PROJECT_ID="your-gcp-project"
REGION="us-central1"
DMS_SQL_INSTANCE="turfai-postgres"
RAG_SQL_INSTANCE="turfai-postgres-rag"
REDIS_HOST="10.0.0.3"  # Get from: gcloud redis instances describe turfai-redis

echo "🚀 Deploying TurfAI to Cloud Run..."
echo "Project: $PROJECT_ID"
echo "Region: $REGION"
echo ""

# 1. Deploy DMS (Public Ingress - API Gateway)
echo "📦 Deploying DMS..."
gcloud run deploy turfai-dms \
  --source ./dms \
  --region $REGION \
  --allow-unauthenticated \
  --platform managed \
  --set-env-vars DATABASE_HOST=/cloudsql/$PROJECT_ID:$REGION:$DMS_SQL_INSTANCE,DATABASE_NAME=turfai_dms,DATABASE_USERNAME=postgres \
  --set-secrets DATABASE_PASSWORD=db-password:latest,GCS_BUCKET_NAME=gcs-bucket:latest \
  --add-cloudsql-instances $PROJECT_ID:$REGION:$DMS_SQL_INSTANCE \
  --memory 1Gi \
  --cpu 1 \
  --min-instances 1 \
  --max-instances 10 \
  --timeout 300 \
  --concurrency 80

DMS_URL=$(gcloud run services describe turfai-dms --region $REGION --format 'value(status.url)')
echo "✅ DMS deployed: $DMS_URL"

# 2. Deploy LLM Service (Private Ingress)
echo "📦 Deploying LLM Service..."
gcloud run deploy turfai-llm \
  --source ./llm-service/api \
  --region $REGION \
  --no-allow-unauthenticated \
  --platform managed \
  --set-secrets OPENAI_API_KEY=openai-api-key:latest,ANTHROPIC_API_KEY=anthropic-api-key:latest,GOOGLE_APPLICATION_CREDENTIALS=gcp-credentials:latest \
  --set-env-vars VERTEX_LOCATION=$REGION \
  --memory 2Gi \
  --cpu 2 \
  --min-instances 0 \
  --max-instances 5 \
  --timeout 300

LLM_URL=$(gcloud run services describe turfai-llm --region $REGION --format 'value(status.url)')
echo "✅ LLM Service deployed: $LLM_URL"

# 3. Deploy Router (Private Ingress)
echo "📦 Deploying Router..."
gcloud run deploy turfai-router \
  --source ./router \
  --region $REGION \
  --no-allow-unauthenticated \
  --platform managed \
  --set-env-vars REDIS_HOST=$REDIS_HOST,REDIS_PORT=6379,DMS_URL=$DMS_URL \
  --set-secrets ROUTER_API_KEY=router-api-key:latest \
  --memory 512Mi \
  --cpu 1 \
  --min-instances 0 \
  --max-instances 10 \
  --timeout 60

ROUTER_URL=$(gcloud run services describe turfai-router --region $REGION --format 'value(status.url)')
echo "✅ Router deployed: $ROUTER_URL"

# 4. Deploy RAG Query Service (Private Ingress)
echo "📦 Deploying RAG Query Service..."
gcloud run deploy turfai-rag-query \
  --source ./rag_query_service \
  --region $REGION \
  --no-allow-unauthenticated \
  --platform managed \
  --set-env-vars POSTGRES_HOST=/cloudsql/$PROJECT_ID:$REGION:$RAG_SQL_INSTANCE,POSTGRES_DB=turfai_rag,POSTGRES_USER=postgres,DMS_URL=$DMS_URL,LLM_SERVICE_URL=$LLM_URL \
  --set-secrets POSTGRES_PASSWORD=db-password:latest \
  --add-cloudsql-instances $PROJECT_ID:$REGION:$RAG_SQL_INSTANCE \
  --memory 1Gi \
  --cpu 1 \
  --min-instances 0 \
  --max-instances 5 \
  --timeout 300

RAG_QUERY_URL=$(gcloud run services describe turfai-rag-query --region $REGION --format 'value(status.url)')
echo "✅ RAG Query deployed: $RAG_QUERY_URL"

# 5. Deploy Processor as Cloud Run Job
echo "📦 Deploying Processor (Cloud Run Job)..."
gcloud run jobs create turfai-processor \
  --source ./processors \
  --region $REGION \
  --set-env-vars REDIS_HOST=$REDIS_HOST,REDIS_PORT=6379,DMS_URL=$DMS_URL,LLM_SERVICE_URL=$LLM_URL \
  --set-secrets GOOGLE_APPLICATION_CREDENTIALS=gcp-credentials:latest \
  --memory 1Gi \
  --cpu 1 \
  --task-timeout 1h \
  --max-retries 3

echo "✅ Processor job created"

# Update DMS with internal service URLs
echo "📝 Updating DMS environment variables..."
gcloud run services update turfai-dms \
  --region $REGION \
  --set-env-vars LLM_SERVICE_URL=$LLM_URL,ROUTER_URL=$ROUTER_URL,RAG_QUERY_SERVICE_URL=$RAG_QUERY_URL

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Deployment Complete!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Public Endpoint:"
echo "  DMS: $DMS_URL"
echo ""
echo "Internal Services (accessible only from DMS):"
echo "  LLM Service:   $LLM_URL"
echo "  Router:        $ROUTER_URL"
echo "  RAG Query:     $RAG_QUERY_URL"
echo ""
echo "Next Steps:"
echo "1. Update frontend to use: $DMS_URL"
echo "2. Run end-to-end tests"
echo "3. View logs: gcloud logging tail"
chmod +x dev/scripts/deploy-cloud-run.sh

Cloud Logging - Viewing Correlated Logs

# View all logs
gcloud logging tail

# View logs for specific service
gcloud logging tail --filter='resource.labels.service_name="turfai-dms"'

# Search by correlation ID
gcloud logging tail --filter='jsonPayload.correlation_id="abc-123"'

# View logs from last hour
gcloud logging tail --since=1h

# Export logs to BigQuery for analysis
gcloud logging sinks create turfai-logs \
  bigquery.googleapis.com/projects/YOUR_PROJECT/datasets/turfai_logs \
  --log-filter='resource.type="cloud_run_revision"'

End-to-End Testing Plan (Day 5)

Test 1: Health Check All Services

# Test DMS
curl http://localhost:1337/_health

# Test LLM Service
curl http://localhost:8080/health

# Test Router
curl http://localhost:9001/health

# Test RAG Query
curl http://localhost:8003/health

Test 2: Document Upload & Processing

# 1. Upload document via DMS
curl -X POST http://localhost:1337/api/upload \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -F "files=@test-document.pdf"

# 2. Check document in DMS
curl http://localhost:1337/api/documents/1 \
  -H "Authorization: Bearer YOUR_TOKEN"

# 3. Enable RAG for document
curl -X POST http://localhost:1337/api/documents/1/enable-rag \
  -H "Authorization: Bearer YOUR_TOKEN"

# 4. Check processing status
curl http://localhost:1337/api/documents/1/rag-status \
  -H "Authorization: Bearer YOUR_TOKEN"

Test 3: RAG Query (End-to-End)

# Query via DMS (which proxies to RAG Query Service)
curl -X POST http://localhost:1337/api/rag/query \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What is the main topic of the document?",
    "session_id": null,
    "top_k": 5
  }'

Expected Flow:

  1. DMS receives request with correlation ID: abc-123
  2. DMS forwards to RAG Query Service with correlation ID
  3. RAG Query generates embedding via LLM Service
  4. RAG Query searches vector database
  5. RAG Query generates answer via LLM Service
  6. DMS generates signed URLs for sources
  7. DMS returns response to client

Check Logs:

# PM2
pm2 logs | grep "abc-123"

# Docker Compose
docker-compose logs -f | grep "abc-123"

# Cloud Run
gcloud logging tail --filter='jsonPayload.correlation_id="abc-123"'

Test 4: LLM Extraction

# Test extraction via DMS
curl -X POST http://localhost:1337/api/llm/extract \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Invoice #12345\nDate: 2025-01-01\nTotal: $1,234.56",
    "schema": {
      "type": "object",
      "properties": {
        "invoice_number": {"type": "string"},
        "date": {"type": "string"},
        "total": {"type": "number"}
      }
    }
  }'

Test 5: Multi-Turn Conversation

# Create session
curl -X POST http://localhost:1337/api/rag/sessions \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test Session"}'
# Returns: {"session_id": "uuid-here"}

# First query
curl -X POST http://localhost:1337/api/rag/query \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What is the name on the Aadhar card?",
    "session_id": "uuid-here"
  }'

# Follow-up query (should use context)
curl -X POST http://localhost:1337/api/rag/query \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "What is the address?",
    "session_id": "uuid-here"
  }'

Quick Reference

Local Development

# Start everything
./dev/scripts/start-local.sh

# View logs
pm2 logs

# View logs for one service
pm2 logs dms

# Restart one service
pm2 restart dms

# Stop everything
./dev/scripts/stop-local.sh

Docker Compose

# Start
docker-compose up -d

# Logs
docker-compose logs -f

# Logs for one service
docker-compose logs -f dms

# Restart one service
docker-compose restart dms

# Stop
docker-compose down

Cloud Run

# Deploy
./dev/scripts/deploy-cloud-run.sh

# View logs
gcloud logging tail

# View logs for one service
gcloud logging tail --filter='resource.labels.service_name="turfai-dms"'

# Search by correlation ID
gcloud logging tail --filter='jsonPayload.correlation_id="abc-123"'

Troubleshooting

Issue: Service won't start

# Check logs
pm2 logs <service-name>

# Check if port is in use
lsof -i :<port>

# Kill process on port
kill -9 <PID>

Issue: Can't connect to database

# Check PostgreSQL is running
pg_isready

# Start PostgreSQL
brew services start postgresql@15

# Check connection
psql -U postgres -l

Issue: Can't connect to Redis

# Check Redis is running
redis-cli ping

# Start Redis
brew services start redis

Issue: Correlation ID not appearing in logs

  • Check middleware is registered
  • Check correlation ID is being forwarded in headers
  • Check logging formatter includes correlation_id field

Success Criteria

After completing this guide, you should have:

✅ Local dev environment that starts with one command ✅ All services logging with correlation IDs ✅ Database auto-initialization ✅ Docker Compose for testing ✅ Cloud Run deployment script ✅ End-to-end test successful ✅ Logs traceable across all services


Next Steps (After Deployment)

  1. ✅ Add classify endpoint to LLM Service
  2. ✅ Add monitoring (Prometheus/Grafana or Cloud Monitoring)
  3. ✅ Add rate limiting
  4. ✅ Add CI/CD pipeline (GitHub Actions)
  5. ✅ Add automated tests
  6. ✅ Performance testing with load

Document Owner: Development Team Review Schedule: After implementation Last Updated: 2025-11-01 Status: Ready for Implementation

On this page