- cd back to SCRIPT_DIR before running docker-compose logs/ps - Fixes "no such file" error when running from frontend/ directory Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
439 lines
14 KiB
Bash
Executable file
439 lines
14 KiB
Bash
Executable file
#!/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:
|
|
# - Run 'git pull origin main' manually before this script
|
|
# - Configure Apache/Nginx reverse proxy separately
|
|
# - Ensure .env file is configured
|
|
|
|
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"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Git info (optional)
|
|
# -----------------------------------------------------------------------------
|
|
if command -v git &> /dev/null && [[ -d "$SCRIPT_DIR/.git" ]]; then
|
|
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")
|
|
log_info "Deploying commit: $COMMIT_HASH"
|
|
log_info "Commit message: $COMMIT_MSG"
|
|
log_info "Commit date: $COMMIT_DATE"
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Build Docker containers
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Building Docker Containers"
|
|
|
|
cd "$SCRIPT_DIR"
|
|
|
|
# Pull latest base images and build
|
|
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"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Stop existing services (graceful shutdown)
|
|
# -----------------------------------------------------------------------------
|
|
log_step "Stopping Existing Services"
|
|
|
|
log_info "Stopping containers gracefully..."
|
|
$DOCKER_COMPOSE -f "$COMPOSE_FILE" down --remove-orphans || log_warn "No existing containers to stop"
|
|
|
|
log_success "Existing services stopped"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 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 "=============================================="
|