#!/bin/bash # ============================================================================== # SmoothSchedule Production Deployment Script # ============================================================================== # # Usage: ./deploy.sh [server] [options] [services...] # # Examples: # ./deploy.sh # Deploy all services # ./deploy.sh --no-migrate # Deploy without migrations # ./deploy.sh django nginx # Deploy specific services # ./deploy.sh --deploy-ap # Build & deploy Activepieces image # ./deploy.sh poduck@server.com # Deploy to custom server # # Options: # --no-migrate Skip database migrations # --deploy-ap Build Activepieces image locally and transfer to server # # Available services: # django, traefik, nginx, postgres, celeryworker, celerybeat, flower, awscli, activepieces # # IMPORTANT: Activepieces Image # ----------------------------- # The production server cannot build the Activepieces image (requires 4GB+ RAM). # Use --deploy-ap to build locally and transfer, or manually: # ./scripts/build-activepieces.sh deploy # # First-time setup: # Run ./smoothschedule/scripts/init-production.sh on the server # ============================================================================== set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Parse arguments SERVER="" SERVICES="" SKIP_MIGRATE=false DEPLOY_AP=false for arg in "$@"; do if [[ "$arg" == "--no-migrate" ]]; then SKIP_MIGRATE=true elif [[ "$arg" == "--deploy-ap" ]]; then DEPLOY_AP=true elif [[ "$arg" == *"@"* ]]; then # Looks like user@host SERVER="$arg" elif [[ -z "$SERVER" && ! "$arg" =~ ^- ]]; then # First non-flag argument could be server or service if [[ "$arg" =~ ^(django|traefik|nginx|postgres|celeryworker|celerybeat|flower|awscli|activepieces|redis|verdaccio)$ ]]; then SERVICES="$SERVICES $arg" else SERVER="$arg" fi else SERVICES="$SERVICES $arg" fi done SERVER=${SERVER:-"poduck@smoothschedule.com"} SERVICES=$(echo "$SERVICES" | xargs) # Trim whitespace REPO_URL="https://git.talova.net/poduck/smoothschedule.git" REMOTE_DIR="/home/poduck/smoothschedule" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" echo -e "${GREEN}===================================" echo "SmoothSchedule Deployment" echo "===================================${NC}" echo "Target server: $SERVER" if [[ -n "$SERVICES" ]]; then echo "Services to rebuild: $SERVICES" else echo "Services to rebuild: ALL" fi if [[ "$SKIP_MIGRATE" == "true" ]]; then echo "Migrations: SKIPPED" fi if [[ "$DEPLOY_AP" == "true" ]]; then echo "Activepieces: BUILDING AND DEPLOYING" fi echo "" # Function to print status print_status() { echo -e "${GREEN}>>> $1${NC}" } print_warning() { echo -e "${YELLOW}>>> $1${NC}" } print_error() { echo -e "${RED}>>> $1${NC}" } # Step 1: Check for uncommitted changes print_status "Step 1: Checking for uncommitted changes..." if [[ -n $(git status --porcelain) ]]; then print_error "You have uncommitted changes. Please commit and push before deploying." git status --short exit 1 fi # Check if local is ahead of remote LOCAL_COMMIT=$(git rev-parse HEAD) REMOTE_COMMIT=$(git rev-parse @{u} 2>/dev/null || echo "") if [[ -z "$REMOTE_COMMIT" ]]; then print_error "No upstream branch configured. Please push your changes first." exit 1 fi if [[ "$LOCAL_COMMIT" != "$REMOTE_COMMIT" ]]; then print_warning "Local branch differs from remote. Checking if ahead..." AHEAD=$(git rev-list --count @{u}..HEAD) if [[ "$AHEAD" -gt 0 ]]; then print_error "You have $AHEAD unpushed commit(s). Please push before deploying." exit 1 fi fi print_status "All changes committed and pushed!" # Step 2: Build and deploy Activepieces image (if requested) if [[ "$DEPLOY_AP" == "true" ]]; then print_status "Step 2: Building and deploying Activepieces image..." # Check if the build script exists if [[ -f "$SCRIPT_DIR/scripts/build-activepieces.sh" ]]; then "$SCRIPT_DIR/scripts/build-activepieces.sh" deploy "$SERVER" else print_warning "Build script not found, building manually..." # Build the image print_status "Building Activepieces Docker image locally..." cd "$SCRIPT_DIR/activepieces-fork" docker build -t smoothschedule_production_activepieces . # Save and transfer print_status "Transferring image to server..." docker save smoothschedule_production_activepieces | gzip > /tmp/ap-image.tar.gz scp /tmp/ap-image.tar.gz "$SERVER:/tmp/" ssh "$SERVER" "gunzip -c /tmp/ap-image.tar.gz | docker load && rm /tmp/ap-image.tar.gz" rm /tmp/ap-image.tar.gz cd "$SCRIPT_DIR" fi print_status "Activepieces image deployed!" fi # Step 3: Deploy on server print_status "Step 3: Deploying on server..." # Set SKIP_AP_BUILD if we already deployed activepieces image SKIP_AP_BUILD_VAL="false" if $DEPLOY_AP; then SKIP_AP_BUILD_VAL="true" fi ssh "$SERVER" "bash -s" << ENDSSH SKIP_AP_BUILD="$SKIP_AP_BUILD_VAL" set -e echo ">>> Setting up project directory..." # Backup .envs if they exist (secrets not in git) if [ -d "$REMOTE_DIR/smoothschedule/.envs" ]; then echo ">>> Backing up .envs secrets..." cp -r "$REMOTE_DIR/smoothschedule/.envs" /tmp/.envs-backup elif [ -d "$REMOTE_DIR/.envs" ]; then # Old structure - .envs was at root level echo ">>> Backing up .envs secrets (old location)..." cp -r "$REMOTE_DIR/.envs" /tmp/.envs-backup fi # Backup .ssh if it exists (SSH keys not in git) if [ -d "$REMOTE_DIR/smoothschedule/.ssh" ]; then echo ">>> Backing up .ssh keys..." cp -r "$REMOTE_DIR/smoothschedule/.ssh" /tmp/.ssh-backup elif [ -d "$REMOTE_DIR/.ssh" ]; then # Old structure echo ">>> Backing up .ssh keys (old location)..." cp -r "$REMOTE_DIR/.ssh" /tmp/.ssh-backup fi if [ ! -d "$REMOTE_DIR/.git" ]; then echo ">>> Cloning repository for the first time..." # Remove old non-git deployment if exists if [ -d "$REMOTE_DIR" ]; then rm -rf "$REMOTE_DIR" fi git clone "$REPO_URL" "$REMOTE_DIR" else echo ">>> Repository exists, pulling latest changes..." cd "$REMOTE_DIR" git fetch origin git reset --hard origin/main fi cd "$REMOTE_DIR" # Restore .envs secrets if [ -d /tmp/.envs-backup ] && [ "$(ls -A /tmp/.envs-backup 2>/dev/null)" ]; then echo ">>> Restoring .envs secrets..." mkdir -p "$REMOTE_DIR/smoothschedule/.envs" cp -r /tmp/.envs-backup/* "$REMOTE_DIR/smoothschedule/.envs/" rm -rf /tmp/.envs-backup fi # Restore .ssh keys if [ -d /tmp/.ssh-backup ] && [ "$(ls -A /tmp/.ssh-backup 2>/dev/null)" ]; then echo ">>> Restoring .ssh keys..." mkdir -p "$REMOTE_DIR/smoothschedule/.ssh" cp -r /tmp/.ssh-backup/* "$REMOTE_DIR/smoothschedule/.ssh/" rm -rf /tmp/.ssh-backup fi echo ">>> Current commit:" git log -1 --oneline cd smoothschedule # Build images (all or specific services) # Note: If activepieces was pre-deployed via --deploy-ap, skip rebuilding it # Use COMPOSE_PARALLEL_LIMIT to reduce memory usage on low-memory servers export COMPOSE_PARALLEL_LIMIT=1 if [[ -n "$SERVICES" ]]; then echo ">>> Building Docker images: $SERVICES..." docker compose -f docker-compose.production.yml build $SERVICES elif [[ "$SKIP_AP_BUILD" == "true" ]]; then # Skip activepieces build since we pre-built and transferred it echo ">>> Building Docker images (excluding activepieces - pre-built)..." docker compose -f docker-compose.production.yml build django nginx traefik postgres celeryworker celerybeat flower awscli verdaccio else echo ">>> Building all Docker images..." docker compose -f docker-compose.production.yml build fi echo ">>> Starting containers..." docker compose -f docker-compose.production.yml up -d echo ">>> Waiting for containers to start..." sleep 5 # Setup Activepieces database (if not exists) echo ">>> Setting up Activepieces database..." AP_DB_USER=\$(grep AP_POSTGRES_USERNAME .envs/.production/.activepieces | cut -d= -f2) AP_DB_PASS=\$(grep AP_POSTGRES_PASSWORD .envs/.production/.activepieces | cut -d= -f2) AP_DB_NAME=\$(grep AP_POSTGRES_DATABASE .envs/.production/.activepieces | cut -d= -f2) # Get the Django postgres user from env file (this is the superuser for our DB) DJANGO_DB_USER=\$(grep POSTGRES_USER .envs/.production/.postgres | cut -d= -f2) DJANGO_DB_USER=\${DJANGO_DB_USER:-postgres} if [ -n "\$AP_DB_USER" ] && [ -n "\$AP_DB_PASS" ] && [ -n "\$AP_DB_NAME" ]; then # Check if user exists, create if not docker compose -f docker-compose.production.yml exec -T postgres psql -U "\$DJANGO_DB_USER" -d postgres -tc "SELECT 1 FROM pg_roles WHERE rolname='\$AP_DB_USER'" | grep -q 1 || { echo " Creating Activepieces database user..." docker compose -f docker-compose.production.yml exec -T postgres psql -U "\$DJANGO_DB_USER" -d postgres -c "CREATE USER \"\$AP_DB_USER\" WITH PASSWORD '\$AP_DB_PASS';" } # Check if database exists, create if not docker compose -f docker-compose.production.yml exec -T postgres psql -U "\$DJANGO_DB_USER" -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='\$AP_DB_NAME'" | grep -q 1 || { echo " Creating Activepieces database..." docker compose -f docker-compose.production.yml exec -T postgres psql -U "\$DJANGO_DB_USER" -d postgres -c "CREATE DATABASE \$AP_DB_NAME OWNER \"\$AP_DB_USER\";" } echo " Activepieces database ready." else echo " Warning: Could not read Activepieces database config from .envs/.production/.activepieces" fi # Wait for Activepieces to be ready echo ">>> Waiting for Activepieces to be ready..." for i in {1..30}; do if curl -s http://localhost:80/api/v1/health 2>/dev/null | grep -q "ok"; then echo " Activepieces is ready." break fi if [ \$i -eq 30 ]; then echo " Warning: Activepieces health check timed out. It may still be starting." fi sleep 2 done # Check if Activepieces platform exists echo ">>> Checking Activepieces platform..." AP_PLATFORM_ID=\$(grep AP_PLATFORM_ID .envs/.production/.activepieces | cut -d= -f2) if [ -z "\$AP_PLATFORM_ID" ] || [ "\$AP_PLATFORM_ID" = "" ]; then echo " WARNING: No AP_PLATFORM_ID configured in .envs/.production/.activepieces" echo " To initialize Activepieces for the first time:" echo " 1. Visit https://automations.smoothschedule.com" echo " 2. Create an admin user (this creates the platform)" echo " 3. Get the platform ID from the response or database" echo " 4. Update AP_PLATFORM_ID in .envs/.production/.activepieces" echo " 5. Also update AP_PLATFORM_ID in .envs/.production/.django" echo " 6. Restart Activepieces: docker compose -f docker-compose.production.yml restart activepieces" else echo " Activepieces platform configured: \$AP_PLATFORM_ID" fi # Run migrations unless skipped if [[ "$SKIP_MIGRATE" != "true" ]]; then echo ">>> Running database migrations..." docker compose -f docker-compose.production.yml exec -T django sh -c 'export DATABASE_URL=postgres://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} && python manage.py migrate' echo ">>> Collecting static files..." docker compose -f docker-compose.production.yml exec -T django sh -c 'export DATABASE_URL=postgres://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} && python manage.py collectstatic --noinput' echo ">>> Seeding/updating platform plugins for all tenants..." docker compose -f docker-compose.production.yml exec -T django sh -c 'export DATABASE_URL=postgres://\${POSTGRES_USER}:\${POSTGRES_PASSWORD}@\${POSTGRES_HOST}:\${POSTGRES_PORT}/\${POSTGRES_DB} && python -c " import django django.setup() from django_tenants.utils import get_tenant_model from django.core.management import call_command Tenant = get_tenant_model() for tenant in Tenant.objects.exclude(schema_name=\"public\"): print(f\" Seeding plugins for {tenant.schema_name}...\") call_command(\"tenant_command\", \"seed_platform_plugins\", schema=tenant.schema_name, verbosity=0) print(\" Done!\") "' else echo ">>> Skipping migrations (--no-migrate flag used)" fi echo ">>> Deployment complete!" ENDSSH echo "" print_status "===================================" print_status "Deployment Complete!" print_status "===================================" echo "" echo "Your application should now be running at:" echo " - https://smoothschedule.com" echo " - https://platform.smoothschedule.com" echo " - https://*.smoothschedule.com (tenant subdomains)" echo " - https://automations.smoothschedule.com (Activepieces)" echo "" echo "To view logs:" echo " ssh $SERVER 'cd ~/smoothschedule/smoothschedule && docker compose -f docker-compose.production.yml logs -f'" echo "" echo "To check status:" echo " ssh $SERVER 'cd ~/smoothschedule/smoothschedule && docker compose -f docker-compose.production.yml ps'"