Files
DreamChat/doc/deployment.md

18 KiB

DreamChat Deployment Guide

Overview

This document covers the deployment configuration including Docker Compose setup, DevContainer configuration, and production deployment procedures.

DevContainer Setup

.devcontainer/devcontainer.json

{
  "name": "DreamChat Development",
  "dockerComposeFile": "docker-compose.yml",
  "service": "app",
  "workspaceFolder": "/workspace",
  "features": {
    "ghcr.io/devcontainers/features/node:1": {
      "version": "20"
    },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss",
        "ms-vscode.vscode-typescript-next",
        "nestjs.vscode-nestjs",
        "prisma.prisma"
      ],
      "settings": {
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode",
        "typescript.preferences.importModuleSpecifier": "relative"
      }
    }
  },
  "forwardPorts": [3000, 5173, 5432, 8080],
  "portsAttributes": {
    "3000": {
      "label": "Backend API",
      "onAutoForward": "notify"
    },
    "5173": {
      "label": "Frontend Dev Server",
      "onAutoForward": "notify"
    },
    "5432": {
      "label": "PostgreSQL",
      "onAutoForward": "silent"
    },
    "8080": {
      "label": "Keycloak",
      "onAutoForward": "notify"
    }
  },
  "postCreateCommand": "bash .devcontainer/post-create.sh",
  "remoteUser": "node",
  "mounts": [
    "source=${localWorkspaceFolderBasename}-node_modules,target=${containerWorkspaceFolder}/node_modules,type=volume",
    "source=${localWorkspaceFolderBasename}-pnpm-store,target=/home/node/.local/share/pnpm/store,type=volume"
  ]
}

.devcontainer/docker-compose.yml

version: '3.8'

services:
  app:
    build:
      context: ..
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - ..:/workspace:cached
      - /var/run/docker.sock:/var/run/docker.sock
    command: sleep infinity
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/dreamchat
      - REDIS_URL=redis://redis:6379
      - KEYCLOAK_URL=http://keycloak:8080
    depends_on:
      - db
      - redis
      - keycloak
    networks:
      - dreamchat-network

  db:
    image: ankane/pgvector:latest
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: dreamchat
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    networks:
      - dreamchat-network

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    ports:
      - "6379:6379"
    networks:
      - dreamchat-network

volumes:
  postgres-data:

networks:
  dreamchat-network:
    driver: bridge

Note: Keycloak is configured as an external service. Set KEYCLOAK_URL in your environment to point to your external Keycloak instance.

.devcontainer/Dockerfile

FROM mcr.microsoft.com/devcontainers/typescript-node:20

# Install additional tools
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    && rm -rf /var/lib/apt/lists/*

# Install pnpm
RUN npm install -g pnpm@8

# Set working directory
WORKDIR /workspace

# Install global packages
RUN npm install -g @nestjs/cli@latest

# Create non-root user
USER node

.devcontainer/post-create.sh

#!/bin/bash
set -e

echo "🚀 Setting up DreamChat monorepo development environment..."

# Install pnpm globally
npm install -g pnpm@8

# Install all dependencies (uses pnpm workspaces)
echo "📦 Installing dependencies..."
cd /workspace
pnpm install

# Build shared packages first
echo "📦 Building shared packages..."
pnpm --filter @dreamchat/shared build

# Generate Prisma client
pnpm db:generate

# Copy environment files if they don't exist
if [ ! -f /workspace/apps/backend/.env ]; then
    echo "⚙️  Creating backend .env file..."
    cat > /workspace/apps/backend/.env << EOF
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://postgres:postgres@db:5432/dreamchat
JWT_SECRET=dev-jwt-secret-change-in-production
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d
LLM_PROVIDER=openrouter
LLM_API_KEY=your-openrouter-api-key
LLM_MODEL=openai/gpt-4o
KEYCLOAK_URL=http://localhost:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
EOF
fi

if [ ! -f /workspace/apps/frontend/.env ]; then
    echo "⚙️  Creating frontend .env file..."
    cat > /workspace/apps/frontend/.env << EOF
VITE_API_URL=http://localhost:3000/api
VITE_WS_URL=ws://localhost:3000
VITE_KEYCLOAK_URL=http://localhost:8080
VITE_KEYCLOAK_REALM=dreamchat
VITE_KEYCLOAK_CLIENT_ID=dreamchat-frontend
EOF
fi

echo "✅ Development environment setup complete!"
echo ""
echo "Next steps:"
echo "1. Start all apps: pnpm dev"
echo "2. Or start individually:"
echo "   - Backend: pnpm --filter @dreamchat/backend dev"
echo "   - Frontend: pnpm --filter @dreamchat/frontend dev"
echo "3. Access Keycloak admin at http://localhost:8080 (admin/admin)"

Docker Compose Production

docker-compose.yml (Root)

version: '3.8'

services:
  # Backend API
  backend:
    build:
      context: .
      dockerfile: apps/backend/Dockerfile
    restart: unless-stopped
    environment:
      - NODE_ENV=production
      - PORT=3000
      - DATABASE_URL=postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/dreamchat
      - JWT_SECRET=${JWT_SECRET}
      - JWT_EXPIRES_IN=${JWT_EXPIRES_IN:-1h}
      - JWT_REFRESH_EXPIRES_IN=${JWT_REFRESH_EXPIRES_IN:-7d}
      - LLM_PROVIDER=${LLM_PROVIDER}
      - LLM_API_KEY=${LLM_API_KEY}
      - LLM_MODEL=${LLM_MODEL}
      - EMBEDDING_PROVIDER=${EMBEDDING_PROVIDER:-local}
      - EMBEDDING_MODEL=${EMBEDDING_MODEL:-Xenova/all-MiniLM-L6-v2}
      - EMBEDDING_DIMENSION=${EMBEDDING_DIMENSION:-384}
      - EMBEDDING_DEVICE=${EMBEDDING_DEVICE:-cpu}
      - HUGGINGFACE_API_KEY=${HUGGINGFACE_API_KEY}
      - KEYCLOAK_URL=${KEYCLOAK_URL}
      - KEYCLOAK_REALM=${KEYCLOAK_REALM}
      - KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID}
      - KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET}
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - backend-logs:/app/logs
      - model-cache:/app/models
    networks:
      - dreamchat-network
    healthcheck:
      test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  # Frontend (static file server)
  # Note: External reverse proxy expected for SSL and routing
  frontend:
    build:
      context: .
      dockerfile: apps/frontend/Dockerfile
    restart: unless-stopped
    ports:
      - "3001:3000"
    environment:
      - VITE_API_URL=/api
      - VITE_WS_URL=/ws
    depends_on:
      - backend
    networks:
      - dreamchat-network

  # Database
  db:
    image: ankane/pgvector:latest
    restart: unless-stopped
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: dreamchat
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    networks:
      - dreamchat-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redis (optional, for session storage and caching)
  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis-data:/data
    networks:
      - dreamchat-network

volumes:
  postgres-data:
  redis-data:
  backend-logs:
  model-cache:  # Persist downloaded embedding models

networks:
  dreamchat-network:
    driver: bridge

Embedding Model Cache (Optional)

To avoid re-downloading models on every restart, models are cached in a Docker volume:

# Add to docker-compose volumes
volumes:
  model-cache:

Models are downloaded on first use and cached at /app/models in the backend container.

Common models and their sizes:

  • Xenova/all-MiniLM-L6-v2: ~80MB (384 dimensions)
  • Xenova/all-mpnet-base-v2: ~420MB (768 dimensions)
  • BAAI/bge-small-en: ~130MB (384 dimensions)

.env.example

# Database
POSTGRES_PASSWORD=your_secure_password_here

# JWT
JWT_SECRET=your_jwt_secret_key_min_32_chars
JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d

# LLM Configuration
LLM_PROVIDER=openrouter
LLM_API_KEY=sk-or-v1-...
LLM_MODEL=openai/gpt-4o

# Embedding Configuration (Local HuggingFace by default)
EMBEDDING_PROVIDER=local
EMBEDDING_MODEL=Xenova/all-MiniLM-L6-v2
EMBEDDING_DIMENSION=384
EMBEDDING_DEVICE=cpu

# HuggingFace API (optional - if not using local embeddings)
# HUGGINGFACE_API_KEY=hf_...

# Keycloak (optional - for external auth)
KEYCLOAK_URL=http://keycloak:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
KEYCLOAK_CLIENT_SECRET=your_keycloak_secret

Backend Dockerfile

# apps/backend/Dockerfile
FROM node:24-alpine AS base
RUN npm install -g pnpm@8

FROM base AS dependencies
WORKDIR /app

# Copy workspace configuration
COPY pnpm-workspace.yaml package.json ./
COPY apps/backend/package.json ./apps/backend/
COPY packages/shared/package.json ./packages/shared/

# Install dependencies
RUN pnpm install --frozen-lockfile

FROM base AS build
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules

# Copy source code
COPY packages/shared ./packages/shared
COPY apps/backend ./apps/backend

# Build shared packages first
RUN pnpm --filter @dreamchat/shared build

# Build backend
RUN pnpm --filter @dreamchat/backend build

FROM base AS production
WORKDIR /app

# Copy only production dependencies
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/apps/backend/node_modules ./apps/backend/node_modules
COPY --from=build /app/apps/backend/dist ./dist
COPY --from=build /app/packages/shared/dist ./node_modules/@dreamchat/shared/dist

# Create logs directory
RUN mkdir -p /app/logs

# Non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

CMD ["node", "dist/main.js"]

Frontend Dockerfile

# apps/frontend/Dockerfile
FROM node:24-alpine AS base
RUN npm install -g pnpm@8

FROM base AS dependencies
WORKDIR /app

# Copy workspace configuration
COPY pnpm-workspace.yaml package.json ./
COPY apps/frontend/package.json ./apps/frontend/
COPY packages/shared/package.json ./packages/shared/

# Install dependencies
RUN pnpm install --frozen-lockfile

FROM base AS build
WORKDIR /app
COPY --from=dependencies /app/node_modules ./node_modules
COPY --from=dependencies /app/apps/frontend/node_modules ./apps/frontend/node_modules
COPY --from=dependencies /app/packages/shared/node_modules ./packages/shared/node_modules

# Copy source code
COPY packages/shared ./packages/shared
COPY apps/frontend ./apps/frontend

# Build shared packages first
RUN pnpm --filter @dreamchat/shared build

# Build frontend
RUN pnpm --filter @dreamchat/frontend build

# Production stage - using serve for static files
# External reverse proxy (nginx/traefik/etc.) expected
FROM node:24-alpine AS production
WORKDIR /app

# Install serve
RUN npm install -g serve

# Copy built assets
COPY --from=build /app/apps/frontend/dist ./dist

# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nodejs -u 1001
USER nodejs

EXPOSE 3000

# Serve static files
# Note: External reverse proxy should handle:
# - SSL/TLS termination
# - Path routing (/api -> backend, / -> frontend)
# - WebSocket proxying
CMD ["serve", "-s", "dist", "-l", "3000"]

External Reverse Proxy Configuration

The frontend container serves static files on port 3000. An external reverse proxy is expected to handle:

  • SSL/TLS termination
  • Path routing:
    • /api/* → Backend (port 3000)
    • /ws → Backend WebSocket (port 3000)
    • /* → Frontend (port 3001)
  • Static file caching

Example nginx configuration:

server {
    listen 443 ssl;
    server_name dreamchat.example.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # API proxy
    location /api {
        proxy_pass http://backend:3000/api;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # WebSocket proxy
    location /ws {
        proxy_pass http://backend:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }

    # Frontend static files
    location / {
        proxy_pass http://frontend:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }
}

Keycloak Configuration (External)

Keycloak is configured as an external service with support for group/role/attribute-based authorization and auto-user creation.

Prerequisites

  1. Have a running Keycloak instance (self-hosted or managed)
  2. Configure the following environment variables in .env:
# Basic Keycloak settings
KEYCLOAK_ENABLED=true
KEYCLOAK_URL=http://your-keycloak-server:8080
KEYCLOAK_REALM=dreamchat
KEYCLOAK_CLIENT_ID=dreamchat-backend
KEYCLOAK_CLIENT_SECRET=your_keycloak_secret

# Authorization settings (optional but recommended)
KEYCLOAK_REQUIRED_GROUP=dreamchat-users
KEYCLOAK_REQUIRED_ROLE=dreamchat-access
KEYCLOAK_REQUIRED_CLIENT_ROLE=user
KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true

# Auto-create users
KEYCLOAK_AUTO_CREATE_USER=true
KEYCLOAK_DEFAULT_USER_ROLE=USER

Keycloak Realm Setup

  1. Access your Keycloak admin console

  2. Create new realm: dreamchat

  3. Create client: dreamchat-backend

    • Client authentication: ON
    • Authorization: ON
    • Valid redirect URIs: http://localhost:3000/*
    • Web origins: http://localhost:3000
  4. Create client: dreamchat-frontend

    • Client authentication: OFF
    • Valid redirect URIs: http://localhost:5173/*
    • Web origins: http://localhost:5173

Authorization Configuration

You can restrict access based on:

1. Group Membership

KEYCLOAK_REQUIRED_GROUP=dreamchat-users

Users must be members of this Keycloak group to access the application.

2. Realm Role

KEYCLOAK_REQUIRED_ROLE=dreamchat-access

Users must have this realm-level role.

3. Client Role

KEYCLOAK_REQUIRED_CLIENT_ROLE=user

Users must have this role for the dreamchat-backend client.

4. User Attribute

KEYCLOAK_REQUIRED_ATTRIBUTE=department:engineering
# or
KEYCLOAK_REQUIRED_ATTRIBUTE=approved:true

Users must have this attribute with the specified value.

User Auto-Creation

When KEYCLOAK_AUTO_CREATE_USER=true:

  • Users are automatically created in the database on first Keycloak login
  • Username is derived from Keycloak preferred_username
  • Email is taken from Keycloak email claim
  • Role is set to KEYCLOAK_DEFAULT_USER_ROLE (default: USER)
  • The keycloakSub field links the local user to Keycloak

When KEYCLOAK_AUTO_CREATE_USER=false:

  • Only existing local users can log in via Keycloak
  • The keycloakSub must match between Keycloak and local user

Example Keycloak Group/Role Setup

  1. Create a group: dreamchat-users
  2. Create a realm role: dreamchat-access
  3. Assign the group and/or role to users who should have access
  4. Configure KEYCLOAK_REQUIRED_GROUP and/or KEYCLOAK_REQUIRED_ROLE

realm-export.json (Optional)

{
  "realm": "dreamchat",
  "enabled": true,
  "clients": [
    {
      "clientId": "dreamchat-backend",
      "enabled": true,
      "clientAuthenticatorType": "client-secret",
      "secret": "**********",
      "redirectUris": ["http://localhost:3000/*"],
      "webOrigins": ["http://localhost:3000"],
      "protocol": "openid-connect"
    },
    {
      "clientId": "dreamchat-frontend",
      "enabled": true,
      "publicClient": true,
      "redirectUris": ["http://localhost:5173/*"],
      "webOrigins": ["http://localhost:5173"],
      "protocol": "openid-connect"
    }
  ]
}

Deployment Procedures

Local Development

# Using DevContainer
1. Open project in VS Code
2. Click "Reopen in Container"
3. Wait for setup to complete
4. Start services:
   - Terminal 1: pnpm --filter @dreamchat/backend dev
   - Terminal 2: pnpm --filter @dreamchat/frontend dev
   - Or run all: pnpm dev

Production Deployment

# 1. Clone repository
git clone https://github.com/yourusername/dreamchat.git
cd dreamchat

# 2. Create environment file
cp .env.example .env
# Edit .env with production values

# 3. Build and start
docker-compose up -d --build

# 4. Run database migrations
docker-compose exec backend pnpm db:migrate

# 5. Check health
curl http://localhost:3000/health

# 6. View logs
docker-compose logs -f backend

Backup and Restore

# Backup database
docker-compose exec db pg_dump -U postgres -Fc dreamchat > backup_$(date +%Y%m%d).dump

# Restore database
docker-compose exec -T db pg_restore -U postgres -d dreamchat < backup_20240223.dump

# Backup volumes
docker run --rm -v dreamchat_postgres-data:/data -v $(pwd):/backup alpine tar czf /backup/postgres-backup.tar.gz -C /data .

Updates

# Pull latest changes
git pull origin main

# Rebuild and restart
docker-compose down
docker-compose up -d --build

# Run migrations
docker-compose exec backend npx prisma migrate deploy

Monitoring and Logging

Health Checks

# Backend health
curl http://localhost:3000/health

# Database health
docker-compose exec db pg_isready -U postgres

# Full stack
docker-compose ps

Log Management

# View all logs
docker-compose logs

# View specific service
docker-compose logs -f backend

# View last 100 lines
docker-compose logs --tail=100 backend

Security Considerations

  1. Change default passwords in production
  2. Use HTTPS with valid SSL certificates
  3. Enable firewall rules for required ports only
  4. Regular updates of base images and dependencies
  5. Secrets management - use Docker secrets or external vault
  6. Network isolation - separate networks for different services

Troubleshooting

Common Issues

  1. Database connection failed

    • Check DATABASE_URL environment variable
    • Ensure db service is healthy: docker-compose ps
  2. WebSocket not connecting

    • Verify VITE_WS_URL matches your domain
    • Check Nginx proxy configuration
  3. Keycloak authentication issues

    • Verify client configuration in Keycloak admin
    • Check redirect URIs match exactly
  4. File upload fails

    • Check file size limits in Nginx config
    • Verify disk space in backend container