solventum-image-metadata/deploy.sh
SamoilenkoVadym 232750d9f8 fix(deploy): make git pull optional for first deployment
- Don't fail if git pull doesn't work (SSH keys not configured)
- Show warning but continue with deployment
- Useful for first-time deployment

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
2026-02-09 15:43:31 +00:00

509 lines
17 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:
# - 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..."
if git pull origin main; then
log_success "Git pull successful"
else
log_warn "Git pull failed - continuing with existing code"
log_warn "This is OK for first deployment or if SSH keys not configured"
log_warn "For updates, ensure git credentials are set up"
fi
# 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 "=============================================="