diff --git a/features/conversation-assistant/Dockerfile.prod b/features/conversation-assistant/Dockerfile.prod new file mode 100644 index 000000000..06c49363b --- /dev/null +++ b/features/conversation-assistant/Dockerfile.prod @@ -0,0 +1,91 @@ +# ============================================================================= +# CONVERSATION ASSISTANT: Production Dockerfile +# ============================================================================= +# Multi-stage build for NestJS server with shared dependencies +# Build from conversation-assistant directory: +# docker build -f Dockerfile.prod -t conversation-assistant . +# ============================================================================= + +FROM node:20-alpine AS base +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache python3 make g++ + +# ============================================================================= +# Dependencies stage +# ============================================================================= +FROM base AS deps + +# Copy package files for all workspaces needed +COPY shared/package*.json ./shared/ +COPY server/package*.json ./server/ + +# Create minimal @lilith/types package.json (dependency stub) +RUN mkdir -p @packages/@core/types && \ + echo '{"name":"@lilith/types","version":"1.0.0","main":"dist/index.js","types":"dist/index.d.ts"}' > @packages/@core/types/package.json + +WORKDIR /app/server + +# Install dependencies +RUN npm install --legacy-peer-deps + +# ============================================================================= +# Build stage +# ============================================================================= +FROM base AS builder +WORKDIR /app + +# Copy shared types +COPY shared ./shared + +# Create stub for @lilith/types (if not all types are needed) +RUN mkdir -p @packages/@core/types/dist && \ + echo 'export {};' > @packages/@core/types/dist/index.js && \ + echo 'export {};' > @packages/@core/types/dist/index.d.ts + +# Copy server source +COPY server ./server + +# Copy dependencies from deps stage +COPY --from=deps /app/server/node_modules ./server/node_modules + +# Build shared first +WORKDIR /app/shared +RUN npm install --legacy-peer-deps && npm run build || true + +# Build server +WORKDIR /app/server +RUN npm run build + +# ============================================================================= +# Production stage +# ============================================================================= +FROM node:20-alpine AS runner +WORKDIR /app + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nestjs + +# Copy built application +COPY --from=builder /app/server/dist ./dist +COPY --from=builder /app/server/node_modules ./node_modules +COPY --from=builder /app/server/package*.json ./ + +# Copy shared (if needed at runtime) +COPY --from=builder /app/shared/dist ../shared/dist +COPY --from=builder /app/shared/package*.json ../shared/ + +# Set ownership +RUN chown -R nestjs:nodejs /app + +USER nestjs + +EXPOSE 3100 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3100/api/health || exit 1 + +CMD ["node", "dist/main"] diff --git a/features/conversation-assistant/deploy.sh b/features/conversation-assistant/deploy.sh new file mode 100755 index 000000000..3fb7b3ab4 --- /dev/null +++ b/features/conversation-assistant/deploy.sh @@ -0,0 +1,202 @@ +#!/bin/bash +# ============================================================================= +# CONVERSATION ASSISTANT: Deploy to Production +# ============================================================================= +# Deploys to 0.1984.dss.nasty.sh (conversations.nasty.sh) +# +# Prerequisites: +# - DNS: conversations.nasty.sh -> 93.95.228.142 +# - SSH access to 0.1984.nasty.sh as root +# +# Usage: +# ./deploy.sh # Full deploy +# ./deploy.sh --build-only # Build images only +# ./deploy.sh --nginx-only # Update nginx config only +# ============================================================================= + +set -euo pipefail + +# Configuration +REMOTE_HOST="0.1984.nasty.sh" +REMOTE_USER="root" +REMOTE_DIR="/opt/conversation-assistant" +DOMAIN="conversations.nasty.sh" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[OK]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +log_error() { echo -e "${RED}[ERROR]${NC} $1"; } + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +check_prerequisites() { + log_info "Checking prerequisites..." + + # Check SSH access + if ! ssh -o ConnectTimeout=5 -o BatchMode=yes "${REMOTE_USER}@${REMOTE_HOST}" 'echo ok' &>/dev/null; then + log_error "Cannot SSH to ${REMOTE_USER}@${REMOTE_HOST}" + exit 1 + fi + log_success "SSH access OK" + + # Check Docker on remote + if ! ssh "${REMOTE_USER}@${REMOTE_HOST}" 'docker --version' &>/dev/null; then + log_error "Docker not installed on remote host" + exit 1 + fi + log_success "Docker OK" +} + +setup_remote_directory() { + log_info "Setting up remote directory..." + + ssh "${REMOTE_USER}@${REMOTE_HOST}" "mkdir -p ${REMOTE_DIR}" + log_success "Created ${REMOTE_DIR}" +} + +sync_files() { + log_info "Syncing files to remote..." + + # Sync project files (excluding node_modules, dist, etc.) + rsync -avz --delete \ + --exclude 'node_modules' \ + --exclude 'dist' \ + --exclude '.turbo' \ + --exclude '.git' \ + --exclude '*.log' \ + --exclude '.env' \ + --exclude '.env.local' \ + ./ "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_DIR}/" + + log_success "Files synced" +} + +create_env_file() { + log_info "Creating .env file on remote..." + + # Generate secrets if not exists + ssh "${REMOTE_USER}@${REMOTE_HOST}" " + cd ${REMOTE_DIR} + if [ ! -f .env ]; then + cat > .env </dev/null || echo 'Nginx config has SSL references - run certbot first' + " + + log_warn "Run certbot manually: sudo certbot --nginx -d ${DOMAIN}" +} + +run_migrations() { + log_info "Running database migrations..." + + ssh "${REMOTE_USER}@${REMOTE_HOST}" " + cd ${REMOTE_DIR} + docker-compose -f docker-compose.prod.yml exec -T server npm run migration:run || echo 'No migrations or migration failed' + " + + log_success "Migrations complete" +} + +show_status() { + log_info "Deployment status:" + + ssh "${REMOTE_USER}@${REMOTE_HOST}" " + cd ${REMOTE_DIR} + docker-compose -f docker-compose.prod.yml ps + echo '' + echo 'Logs (last 20 lines):' + docker-compose -f docker-compose.prod.yml logs --tail=20 server + " +} + +# Main +main() { + echo "" + log_info "Deploying Conversation Assistant to ${DOMAIN}" + echo "" + + case "${1:-}" in + --build-only) + check_prerequisites + sync_files + build_and_start + ;; + --nginx-only) + check_prerequisites + setup_nginx + ;; + *) + check_prerequisites + setup_remote_directory + sync_files + create_env_file + build_and_start + setup_nginx + run_migrations + show_status + ;; + esac + + echo "" + log_success "Deployment complete!" + echo "" + echo "Next steps:" + echo " 1. Add DNS: conversations.nasty.sh -> 93.95.228.142" + echo " 2. Get SSL: ssh ${REMOTE_USER}@${REMOTE_HOST} 'certbot --nginx -d ${DOMAIN}'" + echo " 3. Test: curl https://${DOMAIN}/api/health" + echo "" +} + +main "$@" diff --git a/features/conversation-assistant/docker-compose.prod.yml b/features/conversation-assistant/docker-compose.prod.yml new file mode 100644 index 000000000..8e1679561 --- /dev/null +++ b/features/conversation-assistant/docker-compose.prod.yml @@ -0,0 +1,152 @@ +# ============================================================================= +# CONVERSATION ASSISTANT: Production Deployment +# ============================================================================= +# +# Production stack for conversations.nasty.sh +# Deployed on 0.1984.dss.nasty.sh +# +# Usage: +# docker-compose -f docker-compose.prod.yml up -d +# docker-compose -f docker-compose.prod.yml logs -f server +# docker-compose -f docker-compose.prod.yml down +# +# ============================================================================= + +version: '3.8' + +services: + # ============================================================================= + # SERVER: NestJS API + # ============================================================================= + server: + build: + context: . + dockerfile: Dockerfile.prod + container_name: conversation-assistant-server + restart: unless-stopped + + environment: + NODE_ENV: production + PORT: 3100 + + # Database + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_USER: ${POSTGRES_USER:-conversation} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_NAME: ${POSTGRES_DB:-conversation_assistant} + + # Redis + REDIS_HOST: redis + REDIS_PORT: 6379 + + # JWT + JWT_SECRET: ${JWT_SECRET} + JWT_EXPIRATION: 7d + + # CORS + CORS_ORIGINS: https://conversations.nasty.sh + + ports: + - "127.0.0.1:3100:3100" + + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + healthcheck: + test: ['CMD', 'wget', '--no-verbose', '--tries=1', '--spider', 'http://localhost:3100/api/health'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + networks: + - conversation-net + + # ============================================================================= + # FRONTEND: React Admin Panel + # ============================================================================= + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: https://conversations.nasty.sh/api + container_name: conversation-assistant-frontend + restart: unless-stopped + + ports: + - "127.0.0.1:3101:80" + + depends_on: + - server + + networks: + - conversation-net + + # ============================================================================= + # DATABASE: PostgreSQL + # ============================================================================= + postgres: + image: postgres:16-alpine + container_name: conversation-assistant-postgres + restart: unless-stopped + + environment: + POSTGRES_USER: ${POSTGRES_USER:-conversation} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-conversation_assistant} + + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-conversation}'] + interval: 10s + timeout: 5s + retries: 5 + + networks: + - conversation-net + + # ============================================================================= + # CACHE: Redis + # ============================================================================= + redis: + image: redis:7.4-alpine + container_name: conversation-assistant-redis + restart: unless-stopped + + command: + - redis-server + - --appendonly + - "yes" + - --maxmemory + - "256mb" + - --maxmemory-policy + - "allkeys-lru" + + volumes: + - redis_data:/data + + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 3s + retries: 5 + + networks: + - conversation-net + +volumes: + postgres_data: + name: conversation-assistant-postgres-data + redis_data: + name: conversation-assistant-redis-data + +networks: + conversation-net: + name: conversation-assistant-network diff --git a/features/conversation-assistant/frontend/Dockerfile b/features/conversation-assistant/frontend/Dockerfile new file mode 100644 index 000000000..9a26630be --- /dev/null +++ b/features/conversation-assistant/frontend/Dockerfile @@ -0,0 +1,46 @@ +# ============================================================================= +# CONVERSATION ASSISTANT FRONTEND: Production Dockerfile +# ============================================================================= +# Multi-stage build for Vite React app +# ============================================================================= + +FROM node:20-alpine AS builder +WORKDIR /app + +# Build arguments +ARG VITE_API_URL=https://conversations.nasty.sh/api + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy source +COPY . . + +# Set environment for build +ENV VITE_API_URL=$VITE_API_URL + +# Build +RUN npm run build + +# ============================================================================= +# Production: Nginx to serve static files +# ============================================================================= +FROM nginx:alpine AS runner + +# Copy built files +COPY --from=builder /app/dist /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +# Expose port +EXPOSE 80 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost/health || exit 1 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/features/conversation-assistant/frontend/nginx.conf b/features/conversation-assistant/frontend/nginx.conf new file mode 100644 index 000000000..5d1e275f1 --- /dev/null +++ b/features/conversation-assistant/frontend/nginx.conf @@ -0,0 +1,30 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Health check endpoint + location /health { + access_log off; + return 200 "healthy\n"; + add_header Content-Type text/plain; + } + + # Serve static files with caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + try_files $uri =404; + } + + # SPA routing - serve index.html for all routes + location / { + try_files $uri $uri/ /index.html; + } + + # Gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; + gzip_min_length 1000; +} diff --git a/features/conversation-assistant/nginx/conversations.nasty.sh.conf b/features/conversation-assistant/nginx/conversations.nasty.sh.conf new file mode 100644 index 000000000..0a36fbe72 --- /dev/null +++ b/features/conversation-assistant/nginx/conversations.nasty.sh.conf @@ -0,0 +1,140 @@ +# ============================================================================= +# CONVERSATION ASSISTANT: Nginx Configuration +# ============================================================================= +# conversations.nasty.sh -> conversation-assistant services +# +# Install: +# sudo cp conversations.nasty.sh.conf /etc/nginx/sites-available/ +# sudo ln -sf /etc/nginx/sites-available/conversations.nasty.sh.conf /etc/nginx/sites-enabled/ +# sudo certbot --nginx -d conversations.nasty.sh +# sudo nginx -t && sudo systemctl reload nginx +# ============================================================================= + +# Rate limiting +limit_req_zone $binary_remote_addr zone=conversations_api:10m rate=30r/s; +limit_req_zone $binary_remote_addr zone=conversations_sync:10m rate=10r/s; + +upstream conversation_server { + server 127.0.0.1:3100; + keepalive 32; +} + +upstream conversation_frontend { + server 127.0.0.1:3101; + keepalive 8; +} + +server { + listen 80; + listen [::]:80; + server_name conversations.nasty.sh; + + # Redirect HTTP to HTTPS + location / { + return 301 https://$host$request_uri; + } + + # ACME challenge for Let's Encrypt + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + server_name conversations.nasty.sh; + + # SSL (certbot will update these) + ssl_certificate /etc/letsencrypt/live/conversations.nasty.sh/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/conversations.nasty.sh/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; + + # 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; + + # Logging + access_log /var/log/nginx/conversations.nasty.sh.access.log; + error_log /var/log/nginx/conversations.nasty.sh.error.log; + + # Max body size for sync payloads + client_max_body_size 50M; + + # ============================================================================= + # API Routes + # ============================================================================= + + # Health check (no rate limit) + location = /api/health { + proxy_pass http://conversation_server; + proxy_http_version 1.1; + 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; + } + + # Sync endpoints (stricter rate limit) + location /api/sync { + limit_req zone=conversations_sync burst=20 nodelay; + + proxy_pass http://conversation_server; + proxy_http_version 1.1; + 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; + proxy_set_header Connection ""; + + # Longer timeout for sync operations + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # All other API routes + location /api/ { + limit_req zone=conversations_api burst=50 nodelay; + + proxy_pass http://conversation_server; + proxy_http_version 1.1; + 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; + proxy_set_header Connection ""; + + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } + + # ============================================================================= + # Frontend (Admin Panel) + # ============================================================================= + + location / { + proxy_pass http://conversation_frontend; + proxy_http_version 1.1; + 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 (for HMR in dev, if needed) + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Static assets caching + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + proxy_pass http://conversation_frontend; + proxy_http_version 1.1; + proxy_set_header Host $host; + + expires 1y; + add_header Cache-Control "public, immutable"; + } +} diff --git a/features/conversation-assistant/server/Dockerfile b/features/conversation-assistant/server/Dockerfile new file mode 100644 index 000000000..8725cb9e1 --- /dev/null +++ b/features/conversation-assistant/server/Dockerfile @@ -0,0 +1,70 @@ +# ============================================================================= +# CONVERSATION ASSISTANT SERVER: Production Dockerfile +# ============================================================================= +# Multi-stage build for NestJS server +# ============================================================================= + +FROM node:20-alpine AS base +WORKDIR /app + +# Install dependencies for node-gyp (bcrypt) +RUN apk add --no-cache python3 make g++ + +# ============================================================================= +# Dependencies stage +# ============================================================================= +FROM base AS deps + +# Copy workspace package files +COPY package*.json ./ +COPY ../shared/package*.json ../shared/ + +# Install production dependencies only +RUN npm ci --only=production && npm cache clean --force + +# ============================================================================= +# Build stage +# ============================================================================= +FROM base AS builder + +COPY package*.json ./ +COPY ../shared ../shared +RUN npm ci + +# Copy source and build +COPY tsconfig*.json ./ +COPY nest-cli.json ./ +COPY src ./src + +RUN npm run build + +# ============================================================================= +# Production stage +# ============================================================================= +FROM node:20-alpine AS runner + +WORKDIR /app + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nestjs + +# Copy production dependencies and built app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=builder /app/dist ./dist +COPY package*.json ./ + +# Set ownership +RUN chown -R nestjs:nodejs /app + +USER nestjs + +# Expose port +EXPOSE 3100 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3100/api/health || exit 1 + +# Start server +CMD ["node", "dist/main"]