#!/bin/bash # # Oliver Metadata Tool v4.0 - Production Deployment Script # Idempotent deployment for Ubuntu server at /opt/solventum-image-metadata/ # # Usage: sudo ./deploy.sh # # Prerequisites: # - Configure Apache/Nginx reverse proxy separately # - Ensure .env file is configured # - Git repository must be clean (no uncommitted changes) set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${BLUE}[INFO]${NC} $1" } log_success() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${GREEN}[SUCCESS]${NC} $1" } log_warn() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${YELLOW}[WARN]${NC} $1" } log_error() { echo -e "[$(date '+%Y-%m-%d %H:%M:%S')] ${RED}[ERROR]${NC} $1" } log_step() { echo "" echo -e "${CYAN}▶ $1${NC}" echo "==============================================" } # Error handler error_exit() { log_error "$1" log_error "Deployment failed! Check logs above for details." exit 1 } # Configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FRONTEND_DEPLOY_PATH="/var/www/html/solventum-image-metadata" # Load environment variables to get BACKEND_PORT if [[ -f "$SCRIPT_DIR/.env" ]]; then source "$SCRIPT_DIR/.env" fi BACKEND_PORT="${BACKEND_PORT:-5001}" REDIS_PORT=6379 HEALTH_CHECK_RETRIES=30 HEALTH_CHECK_INTERVAL=2 COMPOSE_FILE="docker-compose.fastapi.yml" # Banner echo "" echo -e "${CYAN}╔════════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║ Oliver Metadata Tool v4.0 Deployment ║${NC}" echo -e "${CYAN}║ FastAPI + React + Redis ║${NC}" echo -e "${CYAN}╚════════════════════════════════════════════════╝${NC}" echo "" log_info "Starting deployment..." log_info "Working directory: $SCRIPT_DIR" log_info "Frontend deploy path: $FRONTEND_DEPLOY_PATH" # ----------------------------------------------------------------------------- # Pre-flight checks # ----------------------------------------------------------------------------- log_step "Pre-flight Checks" # Check if running as root if [[ $EUID -ne 0 ]]; then error_exit "This script must be run as root (use sudo)" fi log_info "✓ Running as root" # Check Docker if ! command -v docker &> /dev/null; then error_exit "Docker is not installed" fi log_info "✓ Docker: $(docker --version)" # Check docker-compose (try both v1 and v2 syntax) if command -v docker-compose &> /dev/null; then DOCKER_COMPOSE="docker-compose" elif docker compose version &> /dev/null; then DOCKER_COMPOSE="docker compose" else error_exit "docker-compose is not installed" fi log_info "✓ Docker Compose: $($DOCKER_COMPOSE version --short 2>/dev/null || $DOCKER_COMPOSE version)" # Check Node.js if ! command -v node &> /dev/null; then error_exit "Node.js is not installed" fi NODE_VERSION=$(node --version) log_info "✓ Node.js: $NODE_VERSION" # Verify Node.js version (need 18+) NODE_MAJOR_VERSION=$(echo "$NODE_VERSION" | sed 's/v\([0-9]*\).*/\1/') if [[ "$NODE_MAJOR_VERSION" -lt 18 ]]; then log_warn "Node.js version $NODE_VERSION detected. Version 18+ recommended." fi # Check npm if ! command -v npm &> /dev/null; then error_exit "npm is not installed" fi log_info "✓ npm: $(npm --version)" # Check git if ! command -v git &> /dev/null; then log_warn "git is not installed - manual code updates required" else log_info "✓ git: $(git --version)" fi # Check .env file if [[ ! -f "$SCRIPT_DIR/.env" ]]; then error_exit "Environment file not found at $SCRIPT_DIR/.env" fi log_info "✓ .env file found" # Validate required environment variables log_info "Validating environment variables..." source "$SCRIPT_DIR/.env" if [[ -z "$SECRET_KEY" ]] || [[ "$SECRET_KEY" == *"change"* ]]; then log_warn "SECRET_KEY not properly set - using default (NOT SECURE FOR PRODUCTION)" fi if [[ -z "$OPENAI_API_KEY" ]]; then log_warn "OPENAI_API_KEY not set - AI features will not work" fi if [[ -n "$AZURE_CLIENT_ID" ]]; then log_info "✓ Azure AD SSO configured" fi # Verify compose file exists if [[ ! -f "$SCRIPT_DIR/$COMPOSE_FILE" ]]; then error_exit "$COMPOSE_FILE not found" fi log_info "✓ Docker Compose file: $COMPOSE_FILE" # Check frontend directory if [[ ! -d "$SCRIPT_DIR/frontend" ]]; then error_exit "Frontend directory not found" fi log_info "✓ Frontend directory exists" # Check backend directory if [[ ! -d "$SCRIPT_DIR/backend" ]]; then error_exit "Backend directory not found" fi log_info "✓ Backend directory exists" log_success "All pre-flight checks passed" # ----------------------------------------------------------------------------- # Pull latest code from Git # ----------------------------------------------------------------------------- log_step "Pulling Latest Code" if command -v git &> /dev/null && [[ -d "$SCRIPT_DIR/.git" ]]; then cd "$SCRIPT_DIR" # Get current commit before pull COMMIT_BEFORE=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") # Check for uncommitted changes if [[ -n $(git status --porcelain 2>/dev/null) ]]; then log_warn "Uncommitted changes detected:" git status --short read -p "Continue with deployment? [y/N] " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then error_exit "Deployment cancelled by user" fi fi # Stash any local changes (just in case) log_info "Stashing local changes (if any)..." git stash push -m "Auto-stash before deployment $(date +%Y%m%d-%H%M%S)" || true # Pull latest code log_info "Pulling from origin/main..." git pull origin main || error_exit "Git pull failed" # Get new commit info COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") COMMIT_MSG=$(git log -1 --pretty=format:"%s" 2>/dev/null || echo "unknown") COMMIT_DATE=$(git log -1 --pretty=format:"%ci" 2>/dev/null || echo "unknown") if [[ "$COMMIT_BEFORE" != "$COMMIT_HASH" ]]; then log_success "Code updated: $COMMIT_BEFORE → $COMMIT_HASH" else log_info "Already up to date at commit: $COMMIT_HASH" fi log_info "Commit message: $COMMIT_MSG" log_info "Commit date: $COMMIT_DATE" else log_warn "Git not available or not a git repository" COMMIT_HASH="unknown" COMMIT_MSG="unknown" COMMIT_DATE="unknown" fi log_success "Code ready for deployment" # ----------------------------------------------------------------------------- # Clean old Docker resources # ----------------------------------------------------------------------------- log_step "Cleaning Old Docker Resources" cd "$SCRIPT_DIR" # Stop old containers log_info "Stopping old containers..." $DOCKER_COMPOSE -f "$COMPOSE_FILE" down --remove-orphans || log_warn "No containers to stop" # Remove old images for this project (keep base images) log_info "Removing old project images..." OLD_IMAGES=$(docker images --filter "reference=solventum-image-metadata*" --filter "reference=*oliver*" -q 2>/dev/null || true) if [[ -n "$OLD_IMAGES" ]]; then docker rmi -f $OLD_IMAGES 2>/dev/null || log_warn "Some images could not be removed (may be in use)" log_success "Old images removed" else log_info "No old images to remove" fi # Clean build cache (keep last 24 hours) log_info "Cleaning Docker build cache..." docker builder prune -f --filter "until=24h" > /dev/null 2>&1 || true # Remove unused networks log_info "Removing unused networks..." docker network prune -f > /dev/null 2>&1 || true # Show disk space saved log_info "Docker cleanup complete" log_success "Old resources cleaned" # ----------------------------------------------------------------------------- # Build Docker containers # ----------------------------------------------------------------------------- log_step "Building Docker Containers" cd "$SCRIPT_DIR" # Pull latest base images and build (use cache for efficiency) log_info "Building containers with latest base images..." $DOCKER_COMPOSE -f "$COMPOSE_FILE" build --pull || error_exit "Docker build failed" log_success "Docker containers built successfully" # ----------------------------------------------------------------------------- # Start Docker services # ----------------------------------------------------------------------------- log_step "Starting Docker Services" log_info "Starting backend and Redis..." $DOCKER_COMPOSE -f "$COMPOSE_FILE" up -d || error_exit "Failed to start Docker services" # Wait for Redis to be ready (inside Docker network) log_info "Waiting for Redis to be ready..." sleep 5 # Give Redis time to start log_success "Redis container started" # Wait for backend to start log_info "Waiting for backend to start..." sleep 5 log_success "Docker services started" # ----------------------------------------------------------------------------- # Database initialization (if needed) # ----------------------------------------------------------------------------- log_step "Database Setup" # Check if database exists if [[ -f "$SCRIPT_DIR/backend/data/oliver_metadata.db" ]]; then log_info "Database file exists - skipping initialization" else log_info "First run detected - database will be initialized automatically" fi # Note: Alembic migrations would go here if we add them # For now, FastAPI initializes DB on first run via init_db() log_success "Database setup complete" # ----------------------------------------------------------------------------- # Build frontend # ----------------------------------------------------------------------------- log_step "Building Frontend" cd "$SCRIPT_DIR/frontend" # Check if node_modules exists and package.json changed if [[ ! -d "node_modules" ]] || [[ "package.json" -nt "node_modules" ]]; then log_info "Installing frontend dependencies..." npm ci || error_exit "npm ci failed" log_success "Dependencies installed" else log_info "Dependencies up to date (skipping install)" fi # Build production bundle log_info "Creating production build with Vite..." npm run build || error_exit "Frontend build failed" # Verify dist directory was created if [[ ! -d "$SCRIPT_DIR/frontend/dist" ]]; then error_exit "Frontend dist directory not found (build failed)" fi # Verify index.html exists if [[ ! -f "$SCRIPT_DIR/frontend/dist/index.html" ]]; then error_exit "Frontend index.html not found in dist/" fi # Get build size BUILD_SIZE=$(du -sh "$SCRIPT_DIR/frontend/dist" | cut -f1) log_info "Build size: $BUILD_SIZE" log_success "Frontend built successfully" # ----------------------------------------------------------------------------- # Deploy frontend to Apache/Nginx # ----------------------------------------------------------------------------- log_step "Deploying Frontend" # Create deployment directory if it doesn't exist log_info "Creating deployment directory..." mkdir -p "$FRONTEND_DEPLOY_PATH" # Backup existing files (optional) if [[ -d "$FRONTEND_DEPLOY_PATH" ]] && [[ "$(ls -A $FRONTEND_DEPLOY_PATH)" ]]; then BACKUP_DIR="/tmp/oliver-metadata-backup-$(date +%Y%m%d-%H%M%S)" log_info "Backing up existing files to $BACKUP_DIR" mkdir -p "$BACKUP_DIR" cp -r "$FRONTEND_DEPLOY_PATH"/* "$BACKUP_DIR/" || log_warn "Backup failed (non-critical)" fi # Clear existing files log_info "Removing old frontend files..." rm -rf "${FRONTEND_DEPLOY_PATH:?}"/* # Copy new build log_info "Copying new build to web directory..." cp -r "$SCRIPT_DIR/frontend/dist/"* "$FRONTEND_DEPLOY_PATH/" # Set proper ownership for web server log_info "Setting permissions..." chown -R www-data:www-data "$FRONTEND_DEPLOY_PATH" chmod -R 755 "$FRONTEND_DEPLOY_PATH" # Verify deployment if [[ ! -f "$FRONTEND_DEPLOY_PATH/index.html" ]]; then error_exit "Frontend deployment verification failed - index.html not found" fi log_success "Frontend deployed to $FRONTEND_DEPLOY_PATH" # ----------------------------------------------------------------------------- # Verification & Health Checks # ----------------------------------------------------------------------------- log_step "Running Health Checks" # Wait for backend API to be ready log_info "Checking backend API health..." BACKEND_READY=false for i in $(seq 1 $HEALTH_CHECK_RETRIES); do HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$BACKEND_PORT/health" 2>/dev/null || echo "000") if [[ "$HTTP_STATUS" == "200" ]]; then BACKEND_READY=true break fi log_info "Waiting for backend... (attempt $i/$HEALTH_CHECK_RETRIES, status: $HTTP_STATUS)" sleep $HEALTH_CHECK_INTERVAL done if [[ "$BACKEND_READY" != "true" ]]; then log_warn "Backend health check failed - service may still be starting" log_info "Backend logs:" cd "$SCRIPT_DIR" $DOCKER_COMPOSE -f "$COMPOSE_FILE" logs --tail=50 backend else log_success "Backend health check passed (HTTP 200)" fi # Check API documentation endpoint API_DOCS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost:$BACKEND_PORT/docs" 2>/dev/null || echo "000") if [[ "$API_DOCS_STATUS" == "200" ]]; then log_success "API docs accessible at http://localhost:$BACKEND_PORT/docs" else log_warn "API docs check failed (status: $API_DOCS_STATUS)" fi # Verify Redis (check if container is running) log_info "Verifying Redis..." if docker ps | grep -q oliver-redis; then log_success "Redis container is running" else log_warn "Redis container not found" fi # Check Docker container status log_info "Docker container status:" cd "$SCRIPT_DIR" $DOCKER_COMPOSE -f "$COMPOSE_FILE" ps # ----------------------------------------------------------------------------- # Cleanup # ----------------------------------------------------------------------------- log_step "Cleanup" # Remove old Docker images log_info "Removing unused Docker images..." docker image prune -f > /dev/null 2>&1 || log_warn "Image cleanup failed (non-critical)" # Remove old backups (keep last 7 days) if [[ -d "/tmp" ]]; then log_info "Removing old backup files (>7 days)..." find /tmp -name "oliver-metadata-backup-*" -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true fi log_success "Cleanup complete" # ----------------------------------------------------------------------------- # Summary # ----------------------------------------------------------------------------- echo "" echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ 🎉 Deployment Successful! ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}" echo "" if [[ -n "$COMMIT_HASH" ]]; then log_info "Deployed commit: $COMMIT_HASH - $COMMIT_MSG" fi echo "" log_info "📍 Access Points:" echo " Frontend: https://ai-sandbox.oliver.solutions/solventum-image-metadata/" echo " Backend API: https://ai-sandbox.oliver.solutions/solventum-image-metadata/api/" echo " API Docs: http://localhost:$BACKEND_PORT/docs" echo "" log_info "🐳 Docker Services:" echo " Backend: http://localhost:$BACKEND_PORT" echo " Redis: localhost:$REDIS_PORT" echo "" log_info "📂 File Locations:" echo " Frontend: $FRONTEND_DEPLOY_PATH" echo " Backend: $SCRIPT_DIR/backend" echo " Database: $SCRIPT_DIR/backend/data/oliver_metadata.db" echo " Uploads: $SCRIPT_DIR/backend/uploads" echo "" log_info "🔧 Useful Commands:" echo " View logs: $DOCKER_COMPOSE -f $COMPOSE_FILE logs -f" echo " Stop services: $DOCKER_COMPOSE -f $COMPOSE_FILE down" echo " Restart backend: $DOCKER_COMPOSE -f $COMPOSE_FILE restart backend" echo " Redis CLI: docker exec -it oliver-redis redis-cli" echo "" if [[ "$BACKEND_READY" != "true" ]]; then log_warn "⚠️ Backend health check did not pass - verify services manually" echo " Check logs: $DOCKER_COMPOSE -f $COMPOSE_FILE logs backend" else log_success "✓ All health checks passed" fi echo "" log_info "🔐 Next Steps:" echo " 1. Configure Apache reverse proxy (see apache-config.conf)" echo " 2. Test frontend: https://ai-sandbox.oliver.solutions/solventum-image-metadata/" echo " 3. Verify SSO redirect (Azure AD)" echo " 4. Upload test files and verify metadata updates" echo "" log_success "Deployment complete! 🚀" echo "=============================================="