Skip to content

Latest commit

 

History

History
836 lines (684 loc) · 21.9 KB

File metadata and controls

836 lines (684 loc) · 21.9 KB
layout title parent nav_order
default
Chapter 8: Production Deployment
Chatbox Tutorial
8

Chapter 8: Production Deployment

Welcome to Chapter 8: Production Deployment. In this part of Chatbox Tutorial: Building Modern AI Chat Interfaces, you will build an intuitive mental model first, then move into concrete implementation details and practical production tradeoffs.

This final chapter covers deploying Chatbox applications to production environments with proper scaling, security, and operational practices.

🚀 Production Architecture

Scalable Deployment

# docker-compose.prod.yml
version: '3.8'
services:
  chatbox:
    image: chatbox/app:latest
    container_name: chatbox_prod
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DATABASE_URL: ${DATABASE_URL}
      REDIS_URL: ${REDIS_URL}
      JWT_SECRET: ${JWT_SECRET}
      API_KEYS: ${API_KEYS}
    volumes:
      - ./uploads:/app/uploads
      - ./logs:/app/logs
    depends_on:
      - postgres
      - redis
    networks:
      - chatbox_network
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '1.0'
          memory: 1G
        reservations:
          cpus: '0.25'
          memory: 256M

  postgres:
    image: postgres:15
    environment:
      POSTGRES_DB: chatbox
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - chatbox_network

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    networks:
      - chatbox_network

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - ./ssl:/etc/ssl/certs
    depends_on:
      - chatbox
    networks:
      - chatbox_network

networks:
  chatbox_network:
    driver: overlay

volumes:
  postgres_data:
  redis_data:

Load Balancing

# nginx.conf for Chatbox
upstream chatbox_backend {
    least_conn;
    server chatbox-1:3000 weight=1 max_fails=3 fail_timeout=30s;
    server chatbox-2:3000 weight=1 max_fails=3 fail_timeout=30s;
    server chatbox-3:3000 weight=1 max_fails=3 fail_timeout=30s;
    keepalive 32;
}

server {
    listen 80;
    server_name chat.yourdomain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name chat.yourdomain.com;

    ssl_certificate /etc/ssl/certs/chat.yourdomain.com.crt;
    ssl_certificate_key /etc/ssl/certs/chat.yourdomain.com.key;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

    location / {
        proxy_pass http://chatbox_backend;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Timeout settings
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }

    # Static file serving
    location /static/ {
        alias /app/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

🔒 Production Security

Environment Configuration

# .env.production
NODE_ENV=production
DATABASE_URL=postgresql://user:secure_password@prod-db:5432/chatbox_prod
REDIS_URL=redis://prod-redis:6379
JWT_SECRET=your-super-secure-jwt-secret-here
SESSION_SECRET=another-very-secure-session-secret

# API Keys (encrypted at rest)
OPENAI_API_KEY=encrypted_key_here
ANTHROPIC_API_KEY=encrypted_key_here

# Security
CORS_ORIGIN=https://chat.yourdomain.com
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX=100

# Monitoring
SENTRY_DSN=https://your-sentry-dsn@sentry.io/project
LOG_LEVEL=warn

Authentication & Authorization

// Production authentication
class ProductionAuthManager {
  private jwtSecret: string
  private sessionStore: SessionStore

  constructor(jwtSecret: string) {
    this.jwtSecret = jwtSecret
    this.sessionStore = new RedisSessionStore()
  }

  async authenticate(credentials: LoginCredentials): Promise<AuthResult> {
    // Rate limiting
    await this.checkRateLimit(credentials.email)

    // Validate credentials
    const user = await this.validateCredentials(credentials)

    if (!user) {
      await this.recordFailedAttempt(credentials.email)
      throw new Error('Invalid credentials')
    }

    // Generate tokens
    const accessToken = this.generateAccessToken(user)
    const refreshToken = await this.generateRefreshToken(user)

    // Create session
    const session = await this.createSession(user, accessToken)

    return {
      user,
      accessToken,
      refreshToken,
      session
    }
  }

  async authorize(token: string, resource: string, action: string): Promise<boolean> {
    try {
      const payload = jwt.verify(token, this.jwtSecret)
      const user = await this.getUserById(payload.userId)

      return this.checkPermission(user, resource, action)
    } catch (error) {
      return false
    }
  }

  private async checkRateLimit(identifier: string): Promise<void> {
    const key = `ratelimit:login:${identifier}`
    const attempts = await this.redis.incr(key)

    if (attempts === 1) {
      await this.redis.expire(key, 900) // 15 minutes
    }

    if (attempts > 5) {
      throw new Error('Too many login attempts')
    }
  }

  private generateAccessToken(user: User): string {
    return jwt.sign(
      {
        userId: user.id,
        email: user.email,
        role: user.role
      },
      this.jwtSecret,
      { expiresIn: '1h' }
    )
  }

  private async generateRefreshToken(user: User): Promise<string> {
    const token = crypto.randomBytes(64).toString('hex')
    const hashedToken = await bcrypt.hash(token, 12)

    await this.storeRefreshToken(user.id, hashedToken)
    return token
  }
}

📊 Monitoring & Observability

Application Metrics

// Production monitoring
class ProductionMonitor {
  private metrics = {
    httpRequests: 0,
    activeConnections: 0,
    messagesProcessed: 0,
    errors: 0,
    responseTime: 0
  }

  recordHttpRequest(method: string, path: string, statusCode: number, duration: number) {
    this.metrics.httpRequests++

    // Record response time
    this.metrics.responseTime = (this.metrics.responseTime + duration) / 2

    // Send to monitoring service
    this.sendMetric('http_request', {
      method,
      path,
      statusCode,
      duration
    })
  }

  recordMessageProcessed(type: string, duration: number) {
    this.metrics.messagesProcessed++

    this.sendMetric('message_processed', {
      type,
      duration
    })
  }

  recordError(error: Error, context: any) {
    this.metrics.errors++

    // Send to error tracking
    this.sendError(error, context)
  }

  getMetrics() {
    return { ...this.metrics }
  }

  private sendMetric(name: string, data: any) {
    // Send to monitoring service (Datadog, New Relic, etc.)
    if (process.env.MONITORING_SERVICE) {
      fetch(process.env.MONITORING_SERVICE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, data, timestamp: new Date() })
      }).catch(err => console.error('Failed to send metric:', err))
    }
  }

  private sendError(error: Error, context: any) {
    // Send to error tracking service
    if (process.env.ERROR_TRACKING_SERVICE) {
      fetch(process.env.ERROR_TRACKING_SERVICE, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          error: error.message,
          stack: error.stack,
          context,
          timestamp: new Date()
        })
      }).catch(err => console.error('Failed to send error:', err))
    }
  }
}

Real-time Dashboards

// Real-time monitoring dashboard
class MonitoringDashboard {
  private widgets: DashboardWidget[] = []

  constructor() {
    this.initializeWidgets()
  }

  private initializeWidgets() {
    this.widgets = [
      {
        id: 'active_users',
        title: 'Active Users',
        type: 'gauge',
        query: 'sum(active_users)',
        thresholds: { warning: 100, critical: 200 }
      },
      {
        id: 'response_time',
        title: 'Average Response Time',
        type: 'line_chart',
        query: 'avg(response_time) by (endpoint)',
        timeRange: '1h'
      },
      {
        id: 'error_rate',
        title: 'Error Rate',
        type: 'line_chart',
        query: 'rate(errors_total[5m])',
        timeRange: '1h',
        thresholds: { warning: 0.05, critical: 0.1 }
      },
      {
        id: 'messages_processed',
        title: 'Messages Processed',
        type: 'counter',
        query: 'sum(messages_processed_total)'
      }
    ]
  }

  async getDashboardData(): Promise<DashboardData> {
    const widgetData = await Promise.all(
      this.widgets.map(widget => this.fetchWidgetData(widget))
    )

    return {
      widgets: widgetData,
      lastUpdated: new Date(),
      refreshInterval: 30000
    }
  }

  private async fetchWidgetData(widget: DashboardWidget): Promise<WidgetData> {
    // Fetch data from monitoring service
    const data = await this.queryMonitoringService(widget.query)

    return {
      id: widget.id,
      title: widget.title,
      type: widget.type,
      data,
      thresholds: widget.thresholds
    }
  }

  private async queryMonitoringService(query: string): Promise<any> {
    // Query monitoring service (Prometheus, etc.)
    const response = await fetch(`${process.env.MONITORING_API}/query?q=${encodeURIComponent(query)}`)
    return response.json()
  }
}

🚀 Scaling Strategies

Horizontal Scaling

// Auto-scaling logic
class AutoScaler {
  private minInstances = 2
  private maxInstances = 10
  private scaleUpThreshold = 0.8 // 80% CPU
  private scaleDownThreshold = 0.3 // 30% CPU
  private cooldownPeriod = 300000 // 5 minutes

  private lastScaleAction = 0

  async evaluateScaling(): Promise<ScalingAction | null> {
    const now = Date.now()

    // Check cooldown period
    if (now - this.lastScaleAction < this.cooldownPeriod) {
      return null
    }

    const metrics = await this.getCurrentMetrics()
    const currentInstances = await this.getCurrentInstanceCount()

    // Scale up logic
    if (metrics.cpu > this.scaleUpThreshold && currentInstances < this.maxInstances) {
      this.lastScaleAction = now
      return {
        action: 'scale_up',
        instances: Math.min(currentInstances + 1, this.maxInstances)
      }
    }

    // Scale down logic
    if (metrics.cpu < this.scaleDownThreshold && currentInstances > this.minInstances) {
      this.lastScaleAction = now
      return {
        action: 'scale_down',
        instances: Math.max(currentInstances - 1, this.minInstances)
      }
    }

    return null
  }

  private async getCurrentMetrics(): Promise<SystemMetrics> {
    // Get current system metrics
    const response = await fetch(`${process.env.MONITORING_API}/metrics`)
    return response.json()
  }

  private async getCurrentInstanceCount(): Promise<number> {
    // Get current instance count from orchestration service
    const response = await fetch(`${process.env.ORCHESTRATION_API}/instances`)
    const data = await response.json()
    return data.length
  }
}

Database Scaling

// Database connection pooling and scaling
class DatabaseScaler {
  private pools: Map<string, Pool> = new Map()
  private maxConnections = 20

  async getConnectionPool(databaseUrl: string): Promise<Pool> {
    if (this.pools.has(databaseUrl)) {
      return this.pools.get(databaseUrl)!
    }

    const pool = new Pool({
      connectionString: databaseUrl,
      max: this.maxConnections,
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 2000
    })

    // Monitor pool health
    this.monitorPoolHealth(pool)

    this.pools.set(databaseUrl, pool)
    return pool
  }

  private monitorPoolHealth(pool: Pool) {
    pool.on('connect', (client) => {
      console.log('New database connection established')
    })

    pool.on('error', (err, client) => {
      console.error('Unexpected error on idle client', err)
    })

    // Monitor pool stats
    setInterval(async () => {
      const stats = {
        totalCount: pool.totalCount,
        idleCount: pool.idleCount,
        waitingCount: pool.waitingCount
      }

      console.log('Pool stats:', stats)

      // Alert if pool is saturated
      if (stats.waitingCount > 5) {
        await this.alertPoolSaturation(stats)
      }
    }, 30000)
  }

  private async alertPoolSaturation(stats: any) {
    // Send alert to monitoring system
    console.error('Database connection pool saturated:', stats)
  }
}

💾 Backup & Recovery

Automated Backups

#!/bin/bash
# production_backup.sh

BACKUP_DIR="/opt/backups/chatbox"
DATE=$(date +%Y%m%d_%H%M%S)
RETENTION_DAYS=30

# Create backup directory
mkdir -p $BACKUP_DIR

# Database backup
docker exec chatbox_postgres pg_dump -U chatbox chatbox > $BACKUP_DIR/db_$DATE.sql

# Application data backup
docker run --rm -v chatbox_uploads:/data -v $BACKUP_DIR:/backup alpine tar czf /backup/uploads_$DATE.tar.gz -C /data .

# Redis backup (if using persistence)
docker exec chatbox_redis redis-cli save
docker cp chatbox_redis:/data/dump.rdb $BACKUP_DIR/redis_$DATE.rdb

# Upload to cloud storage
aws s3 cp $BACKUP_DIR/db_$DATE.sql s3://chatbox-backups/database/
aws s3 cp $BACKUP_DIR/uploads_$DATE.tar.gz s3://chatbox-backups/uploads/
aws s3 cp $BACKUP_DIR/redis_$DATE.rdb s3://chatbox-backups/redis/

# Clean old backups
find $BACKUP_DIR -name "*.sql" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
find $BACKUP_DIR -name "*.rdb" -mtime +$RETENTION_DAYS -delete

echo "Backup completed: $DATE"

Disaster Recovery

// Disaster recovery procedures
class DisasterRecoveryManager {
  private recoveryPlans: Map<string, RecoveryPlan> = new Map()

  constructor() {
    this.initializeRecoveryPlans()
  }

  private initializeRecoveryPlans() {
    this.recoveryPlans.set('database_failure', {
      steps: [
        'Detect database unavailability',
        'Switch to read-only mode',
        'Attempt database restart',
        'Failover to backup database',
        'Restore from latest backup if needed'
      ],
      estimatedTime: 1800000, // 30 minutes
      rto: 300000, // 5 minutes
      rpo: 3600000 // 1 hour
    })

    this.recoveryPlans.set('application_failure', {
      steps: [
        'Detect application unavailability',
        'Check load balancer health',
        'Restart failed instances',
        'Scale up if needed',
        'Rollback to previous version if restart fails'
      ],
      estimatedTime: 600000, // 10 minutes
      rto: 120000, // 2 minutes
      rpo: 0 // No data loss
    })
  }

  async initiateRecovery(scenario: string): Promise<RecoveryResult> {
    const plan = this.recoveryPlans.get(scenario)
    if (!plan) {
      throw new Error(`No recovery plan for scenario: ${scenario}`)
    }

    console.log(`Initiating disaster recovery for: ${scenario}`)

    const result = {
      scenario,
      startTime: new Date(),
      steps: [],
      status: 'in_progress'
    }

    try {
      for (const step of plan.steps) {
        console.log(`Executing: ${step}`)
        const stepResult = await this.executeRecoveryStep(step)
        result.steps.push(stepResult)

        if (!stepResult.success) {
          result.status = 'failed'
          break
        }
      }

      result.status = 'completed'

    } catch (error) {
      result.status = 'error'
      result.error = error.message
    }

    result.endTime = new Date()
    return result
  }

  private async executeRecoveryStep(step: string): Promise<StepResult> {
    try {
      // Execute specific recovery step
      switch (step) {
        case 'Detect database unavailability':
          return await this.checkDatabaseHealth()
        case 'Switch to read-only mode':
          return await this.enableReadOnlyMode()
        case 'Attempt database restart':
          return await this.restartDatabase()
        default:
          return { step, success: true, message: 'Step completed' }
      }
    } catch (error) {
      return { step, success: false, error: error.message }
    }
  }

  async testRecoveryPlans(): Promise<TestResult[]> {
    const results = []

    for (const [scenario, plan] of this.recoveryPlans) {
      const testResult = await this.testRecoveryPlan(scenario, plan)
      results.push(testResult)
    }

    return results
  }

  private async testRecoveryPlan(scenario: string, plan: RecoveryPlan): Promise<TestResult> {
    // Simulate recovery without actual execution
    return {
      scenario,
      success: true,
      duration: plan.estimatedTime,
      testedAt: new Date()
    }
  }
}

📊 Performance Optimization

Caching Strategy

// Production caching implementation
class ProductionCacheManager {
  private redis: Redis
  private localCache: Map<string, any> = new Map()

  constructor(redisUrl: string) {
    this.redis = new Redis(redisUrl)
  }

  async get<T>(key: string, ttl?: number): Promise<T | null> {
    // Try local cache first
    if (this.localCache.has(key)) {
      return this.localCache.get(key)
    }

    // Try Redis
    const cached = await this.redis.get(key)
    if (cached) {
      const parsed = JSON.parse(cached)
      // Store in local cache for faster access
      this.localCache.set(key, parsed)
      return parsed
    }

    return null
  }

  async set(key: string, value: any, ttl: number = 300): Promise<void> {
    const serialized = JSON.stringify(value)

    // Store in both caches
    this.localCache.set(key, value)
    await this.redis.setex(key, ttl, serialized)
  }

  async invalidate(pattern: string): Promise<void> {
    // Clear local cache
    for (const key of this.localCache.keys()) {
      if (key.includes(pattern)) {
        this.localCache.delete(key)
      }
    }

    // Clear Redis cache
    const keys = await this.redis.keys(`*${pattern}*`)
    if (keys.length > 0) {
      await this.redis.del(...keys)
    }
  }

  async getStats(): Promise<CacheStats> {
    const info = await this.redis.info()
    const localSize = this.localCache.size

    return {
      redis: {
        connectedClients: parseInt(info.connected_clients),
        usedMemory: info.used_memory_human,
        hits: parseInt(info.keyspace_hits),
        misses: parseInt(info.keyspace_misses)
      },
      local: {
        size: localSize
      }
    }
  }
}

📝 Chapter Summary

  • ✅ Built scalable production architecture
  • ✅ Implemented comprehensive security measures
  • ✅ Set up monitoring and observability
  • ✅ Created auto-scaling and load balancing
  • ✅ Established backup and disaster recovery
  • ✅ Optimized performance with caching

Key Takeaways:

  • Production deployment requires careful planning
  • Security must be implemented at every layer
  • Monitoring enables proactive issue resolution
  • Scaling should handle variable loads
  • Backups and recovery are critical for reliability
  • Performance optimization is ongoing
  • Disaster recovery planning prevents data loss

What Problem Does This Solve?

Most teams struggle here because the hard part is not writing more code, but deciding clear boundaries for error, Promise, redis so behavior stays predictable as complexity grows.

In practical terms, this chapter helps you avoid three common failures:

  • coupling core logic too tightly to one implementation path
  • missing the handoff boundaries between setup, execution, and validation
  • shipping changes without clear rollback or observability strategy

After working through this chapter, you should be able to reason about Chapter 8: Production Deployment as an operating subsystem inside Chatbox Tutorial: Building Modern AI Chat Interfaces, with explicit contracts for inputs, state transitions, and outputs.

Use the implementation notes around user, pool, chatbox as your checklist when adapting these patterns to your own repository.

How it Works Under the Hood

Under the hood, Chapter 8: Production Deployment usually follows a repeatable control path:

  1. Context bootstrap: initialize runtime config and prerequisites for error.
  2. Input normalization: shape incoming data so Promise receives stable contracts.
  3. Core execution: run the main logic branch and propagate intermediate state through redis.
  4. Policy and safety checks: enforce limits, auth scopes, and failure boundaries.
  5. Output composition: return canonical result payloads for downstream consumers.
  6. Operational telemetry: emit logs/metrics needed for debugging and performance tuning.

When debugging, walk this sequence in order and confirm each stage has explicit success/failure conditions.

Source Walkthrough

Use the following upstream sources to verify implementation details while reading this chapter:

  • View Repo Why it matters: authoritative reference on View Repo (github.com).

Suggested trace strategy:

  • search upstream code for error and Promise to map concrete implementation paths
  • compare docs claims against actual runtime/config code before reusing patterns in production

Chapter Connections