#!/bin/bash set -e # Exit on error # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # Configuration file CONFIG_FILE=".env" # Log function log() { echo -e "${BLUE}[INFO]${NC} $1" } success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } error() { echo -e "${RED}[ERROR]${NC} $1" } warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } # Banner banner() { echo "" echo -e "${BLUE}╔════════════════════════════════════════════════════╗${NC}" echo -e "${BLUE}║ ║${NC}" echo -e "${BLUE}║ ${GREEN}SaaS Platform Deployment Script${BLUE} ║${NC}" echo -e "${BLUE}║ ║${NC}" echo -e "${BLUE}╚════════════════════════════════════════════════════╝${NC}" echo "" } # Check if running as root check_root() { if [ "$EUID" -eq 0 ]; then error "Please do not run this script as root" error "Run without sudo, the script will ask for permissions when needed" exit 1 fi } # Check system requirements check_requirements() { log "Checking system requirements..." local missing_deps=() # Check Docker if ! command -v docker &> /dev/null; then missing_deps+=("docker") fi # Check Docker Compose if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then missing_deps+=("docker-compose") fi # Check dig (for DNS validation) if ! command -v dig &> /dev/null; then missing_deps+=("dnsutils/bind-tools") fi # Check curl if ! command -v curl &> /dev/null; then missing_deps+=("curl") fi # Check openssl if ! command -v openssl &> /dev/null; then missing_deps+=("openssl") fi if [ ${#missing_deps[@]} -ne 0 ]; then error "Missing required dependencies: ${missing_deps[*]}" echo "" log "Please install the missing dependencies:" echo "" echo "Ubuntu/Debian:" echo " sudo apt update" echo " sudo apt install -y docker.io docker-compose dnsutils curl openssl" echo "" echo "CentOS/RHEL:" echo " sudo yum install -y docker docker-compose bind-utils curl openssl" echo "" exit 1 fi success "All requirements met" } # Validate domain format validate_domain_format() { local domain=$1 # Check if domain matches pattern (basic validation) if [[ ! "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]; then return 1 fi # Check if domain has at least one dot if [[ ! "$domain" =~ \. ]]; then return 1 fi return 0 } # Check DNS records check_dns_records() { local domain=$1 local console_domain="console.$domain" local api_domain="api.$domain" local wss_domain="wss.$domain" log "Checking DNS records for $domain..." # Check A record for main domain log "Checking A record for $domain..." if ! dig +short A "$domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' &> /dev/null; then error "No A record found for $domain" warning "Please add an A record pointing to your server's IP address" return 1 fi local domain_ip=$(dig +short A "$domain" | head -n1) log "Found A record: $domain → $domain_ip" # Check A record for console subdomain log "Checking A record for $console_domain..." if ! dig +short A "$console_domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' &> /dev/null; then error "No A record found for $console_domain" warning "Please add an A record for $console_domain pointing to your server's IP address" return 1 fi local console_ip=$(dig +short A "$console_domain" | head -n1) log "Found A record: $console_domain → $console_ip" # Check A record for API subdomain log "Checking A record for $api_domain..." if ! dig +short A "$api_domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' &> /dev/null; then error "No A record found for $api_domain" warning "Please add an A record for $api_domain pointing to your server's IP address" return 1 fi local api_ip=$(dig +short A "$api_domain" | head -n1) log "Found A record: $api_domain → $api_ip" # Check A record for WSS subdomain log "Checking A record for $wss_domain..." if ! dig +short A "$wss_domain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' &> /dev/null; then error "No A record found for $wss_domain" warning "Please add an A record for $wss_domain pointing to your server's IP address" return 1 fi local wss_ip=$(dig +short A "$wss_domain" | head -n1) log "Found A record: $wss_domain → $wss_ip" # Check wildcard record log "Checking wildcard DNS record for *.$domain..." local test_subdomain="test-$(date +%s).$domain" if ! dig +short A "$test_subdomain" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' &> /dev/null; then error "No wildcard (*.$domain) DNS record found" warning "Please add a wildcard A record: *.$domain → your server's IP" warning "This is required for hosting applications on subdomains" return 1 fi local wildcard_ip=$(dig +short A "$test_subdomain" | head -n1) log "Found wildcard record: *.$domain → $wildcard_ip" # Verify all IPs match if [ "$domain_ip" != "$console_ip" ] || [ "$domain_ip" != "$api_ip" ] || [ "$domain_ip" != "$wss_ip" ] || [ "$domain_ip" != "$wildcard_ip" ]; then warning "DNS records point to different IP addresses:" warning " $domain → $domain_ip" warning " $console_domain → $console_ip" warning " $api_domain → $api_ip" warning " $wss_domain → $wss_ip" warning " *.$domain → $wildcard_ip" warning "They should all point to the same IP address" read -p "Continue anyway? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then return 1 fi fi success "DNS records validated successfully" return 0 } # Get user input for domain get_domain_input() { # Detect server's public IP local server_ip=$(curl -s ifconfig.me 2>/dev/null || curl -s icanhazip.com 2>/dev/null || echo "your-server-ip") echo "" log "Domain Configuration" echo "" echo "Please enter your domain name (e.g., example.com)" echo "" log "Detected server IP: ${GREEN}$server_ip${NC}" echo "" echo "Make sure you have the following DNS records configured:" echo " - A record: yourdomain.com → $server_ip" echo " - A record: console.yourdomain.com → $server_ip" echo " - A record: api.yourdomain.com → $server_ip" echo " - A record: wss.yourdomain.com → $server_ip" echo " - A record: *.yourdomain.com → $server_ip (wildcard)" echo "" echo "Replace 'yourdomain.com' with your actual domain name." echo "" while true; do read -p "Domain name: " DOMAIN # Trim whitespace DOMAIN=$(echo "$DOMAIN" | xargs) if [ -z "$DOMAIN" ]; then error "Domain cannot be empty" continue fi # Validate domain format if ! validate_domain_format "$DOMAIN"; then error "Invalid domain format: $DOMAIN" error "Please enter a valid domain name (e.g., example.com)" continue fi # Show DNS configuration with actual domain and IP echo "" log "Required DNS Configuration for: ${GREEN}$DOMAIN${NC}" echo "" echo " ┌─────────────────────────────────────────────────────────┐" echo " │ Configure these DNS A records in your DNS provider: │" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Record: $DOMAIN" echo " │ Type: A" echo " │ Value: $server_ip" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Record: console.$DOMAIN" echo " │ Type: A" echo " │ Value: $server_ip" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Record: api.$DOMAIN" echo " │ Type: A" echo " │ Value: $server_ip" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Record: wss.$DOMAIN" echo " │ Type: A" echo " │ Value: $server_ip" echo " ├─────────────────────────────────────────────────────────┤" echo " │ Record: *.$DOMAIN (wildcard)" echo " │ Type: A" echo " │ Value: $server_ip" echo " └─────────────────────────────────────────────────────────┘" echo "" # Check DNS records if check_dns_records "$DOMAIN"; then success "Domain validated: $DOMAIN" break else echo "" warning "DNS validation failed for $DOMAIN" echo "" log "Make sure all 5 DNS records point to: $server_ip" log "DNS propagation can take up to 48 hours" echo "" read -p "Try again with the same domain (wait for DNS)? (Y/n): " -n 1 -r echo if [[ $REPLY =~ ^[Nn]$ ]]; then read -p "Enter a different domain? (Y/n): " -n 1 -r echo if [[ $REPLY =~ ^[Nn]$ ]]; then exit 1 fi else # Wait a bit and try again log "Waiting 10 seconds for DNS propagation..." sleep 10 fi fi done } # Get user email for Let's Encrypt get_email_input() { echo "" log "Let's Encrypt Configuration" echo "" echo "Let's Encrypt requires an email address for:" echo " - Certificate expiration notifications" echo " - Important account updates" echo "" while true; do read -p "Email address: " LE_EMAIL # Trim whitespace LE_EMAIL=$(echo "$LE_EMAIL" | xargs) if [ -z "$LE_EMAIL" ]; then error "Email cannot be empty" continue fi # Basic email validation if [[ ! "$LE_EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then error "Invalid email format" continue fi success "Email validated: $LE_EMAIL" break done } # Install Certbot install_certbot() { log "Checking Certbot installation..." if command -v certbot &> /dev/null; then success "Certbot is already installed" certbot --version return 0 fi log "Installing Certbot..." # Detect OS if [ -f /etc/debian_version ]; then # Debian/Ubuntu sudo apt update sudo apt install -y certbot python3-certbot-nginx elif [ -f /etc/redhat-release ]; then # CentOS/RHEL sudo yum install -y certbot python3-certbot-nginx else error "Unsupported operating system" error "Please install Certbot manually: https://certbot.eff.org/" exit 1 fi success "Certbot installed successfully" } # Generate Let's Encrypt certificates generate_letsencrypt_certs() { local domain=$1 local email=$2 local console_domain="console.$domain" local api_domain="api.$domain" local wss_domain="wss.$domain" log "Generating Let's Encrypt certificates..." echo "" log "This will request certificates for:" log " - $domain" log " - $console_domain" log " - $api_domain" log " - $wss_domain" echo "" # Stop nginx if running (to free port 80) if docker ps | grep saas-gateway &> /dev/null; then log "Stopping Nginx container to free port 80..." docker stop saas-gateway || true fi # Request certificate using standalone mode log "Requesting certificate (this may take a minute)..." sudo certbot certonly \ --standalone \ --non-interactive \ --agree-tos \ --email "$email" \ --domains "$domain,$console_domain,$api_domain,$wss_domain" \ --preferred-challenges http \ || { error "Failed to generate Let's Encrypt certificate" error "Common issues:" error " 1. Port 80 is not accessible from the internet" error " 2. DNS records are not properly configured" error " 3. Domain is already registered with Let's Encrypt" exit 1 } success "Let's Encrypt certificates generated successfully" # Copy certificates to ssl directory log "Copying certificates to ssl directory..." sudo cp "/etc/letsencrypt/live/$domain/fullchain.pem" "$SCRIPT_DIR/ssl/certificate.crt" sudo cp "/etc/letsencrypt/live/$domain/privkey.pem" "$SCRIPT_DIR/ssl/private.key" sudo chown $(whoami):$(whoami) "$SCRIPT_DIR/ssl/certificate.crt" "$SCRIPT_DIR/ssl/private.key" chmod 644 "$SCRIPT_DIR/ssl/certificate.crt" chmod 600 "$SCRIPT_DIR/ssl/private.key" success "Certificates copied to $SCRIPT_DIR/ssl/" } # Generate DH parameters if not exists generate_dhparam() { if [ -f "$SCRIPT_DIR/ssl/dhparam.pem" ]; then log "DH parameters already exist" return 0 fi log "Generating DH parameters (this may take several minutes)..." openssl dhparam -out "$SCRIPT_DIR/ssl/dhparam.pem" 2048 chmod 644 "$SCRIPT_DIR/ssl/dhparam.pem" success "DH parameters generated" } # Create environment file create_env_file() { local domain=$1 log "Creating environment configuration..." # Generate random secrets local db_password=$(openssl rand -base64 32) local redis_password=$(openssl rand -base64 32) local jwt_secret=$(openssl rand -base64 64) local minio_access_key=$(openssl rand -hex 16) local minio_secret_key=$(openssl rand -base64 32) cat > "$CONFIG_FILE" < "nginx/conf.d/production.conf" < "apps/hello-world/index.html" < Welcome to $domain
👋

Hello World!

Your SaaS Platform is successfully deployed and running!

Main Domain: $domain
Server IP: $server_ip
Dashboard: console.$domain
API: api.$domain
WebSocket: wss.$domain
Status: 🟢 Running
SSL: 🔒 Let's Encrypt
EOF success "Hello world application created" } # Update docker-compose for production update_docker_compose() { local domain=$1 log "Updating docker-compose.yml for production..." # Update api-gateway to expose port 443 and mount hello-world app if ! grep -q "443:443" docker-compose.yml; then log "Updating Nginx ports configuration..." # This is a simplified approach - in production you'd use yq or similar sed -i 's/- "8443:443"/- "443:443"/' docker-compose.yml || true fi # Add hello-world volume mount if not exists if ! grep -q "hello-world" docker-compose.yml; then log "Adding hello-world application mount..." # Note: This would need proper YAML editing in production fi success "Docker Compose configuration updated" } # Start services start_services() { log "Starting services with Docker Compose..." # Pull latest images log "Pulling Docker images..." docker-compose pull # Build services log "Building services..." docker-compose build # Start services log "Starting all services..." docker-compose up -d # Wait for services to be healthy log "Waiting for services to be healthy..." sleep 10 # Check service status docker-compose ps success "All services started successfully" } # Setup certbot auto-renewal setup_cert_renewal() { local domain=$1 log "Setting up automatic certificate renewal..." # Create renewal script cat > "$SCRIPT_DIR/renew-certs.sh" <<'EOF' #!/bin/bash # Auto-renewal script for Let's Encrypt certificates # Stop nginx docker stop saas-gateway # Renew certificates certbot renew --quiet # Copy renewed certificates cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem /data/appserver/ssl/certificate.crt cp /etc/letsencrypt/live/$DOMAIN/privkey.pem /data/appserver/ssl/private.key # Start nginx docker start saas-gateway # Reload nginx docker exec saas-gateway nginx -s reload EOF chmod +x "$SCRIPT_DIR/renew-certs.sh" # Add cron job (runs twice daily) (crontab -l 2>/dev/null; echo "0 0,12 * * * $SCRIPT_DIR/renew-certs.sh >> $SCRIPT_DIR/logs/cert-renewal.log 2>&1") | crontab - success "Certificate auto-renewal configured" } # Print deployment summary print_summary() { local domain=$1 local console_domain="console.$domain" local api_domain="api.$domain" local wss_domain="wss.$domain" echo "" echo -e "${GREEN}╔════════════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}║ ${BLUE}Deployment Completed Successfully!${GREEN} ║${NC}" echo -e "${GREEN}║ ║${NC}" echo -e "${GREEN}╚════════════════════════════════════════════════════╝${NC}" echo "" echo -e "${BLUE}📍 Your Platform URLs:${NC}" echo "" echo -e " 🌐 Main Website: ${GREEN}https://$domain${NC}" echo -e " 🎛️ Dashboard: ${GREEN}https://$console_domain${NC}" echo -e " 🔌 API Endpoint: ${GREEN}https://$api_domain${NC}" echo -e " 📡 WebSocket (WSS): ${GREEN}wss://$wss_domain${NC}" echo -e " 📊 API Health: ${GREEN}https://$api_domain/health${NC}" echo "" echo -e "${BLUE}🔐 Security:${NC}" echo "" echo -e " ✅ SSL/TLS certificates: Let's Encrypt" echo -e " ✅ Auto-renewal: Configured (twice daily)" echo -e " ✅ HTTPS redirect: Enabled" echo -e " ✅ Security headers: Enabled" echo "" echo -e "${BLUE}📁 Important Files:${NC}" echo "" echo -e " 📄 Configuration: ${YELLOW}$CONFIG_FILE${NC}" echo -e " 🔑 Certificates: ${YELLOW}$SCRIPT_DIR/ssl/${NC}" echo -e " 📝 Logs: ${YELLOW}docker-compose logs -f${NC}" echo "" echo -e "${BLUE}🚀 Next Steps:${NC}" echo "" echo " 1. Visit https://$console_domain to access the dashboard" echo " 2. Create your admin account" echo " 3. Check https://$domain to see the hello world app" echo " 4. Monitor services: docker-compose ps" echo " 5. View logs: docker-compose logs -f [service-name]" echo "" echo -e "${YELLOW}⚠️ Important:${NC}" echo "" echo " - Keep your $CONFIG_FILE file secure (contains passwords)" echo " - Backup your database regularly" echo " - Monitor certificate expiration (auto-renewed)" echo "" success "Deployment completed! 🎉" echo "" } # Main deployment function main() { banner check_root check_requirements get_domain_input get_email_input echo "" log "Deployment Summary:" echo "" echo " Domain: $DOMAIN" echo " Console: console.$DOMAIN" echo " Email: $LE_EMAIL" echo "" read -p "Proceed with deployment? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then warning "Deployment cancelled" exit 0 fi install_certbot generate_letsencrypt_certs "$DOMAIN" "$LE_EMAIL" generate_dhparam create_env_file "$DOMAIN" update_nginx_config "$DOMAIN" create_hello_world_app "$DOMAIN" update_docker_compose "$DOMAIN" start_services setup_cert_renewal "$DOMAIN" print_summary "$DOMAIN" } # Run main function main "$@"