Files
smoothschedule/deploy.sh
poduck 8564b1deba Fix deploy script parallel build syntax
- Use COMPOSE_PARALLEL_LIMIT env var instead of --parallel flag
- Fix SKIP_AP_BUILD variable passing in heredoc

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-21 15:52:53 -05:00

350 lines
13 KiB
Bash
Executable File

#!/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'"