#!/bin/bash # One-Command Production Setup for Custom PHP Framework # Complete automated setup from server preparation to deployment completion # Domain: michaelschiemer.de | Email: kontakt@michaelschiemer.de | PHP: 8.4 # Usage: ./setup-production.sh [options] set -euo pipefail # Script configuration SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../" && pwd)" DEPLOYMENT_DIR="${SCRIPT_DIR}" # Default configuration SERVER_IP="94.16.110.151" DOMAIN="michaelschiemer.de" EMAIL="kontakt@michaelschiemer.de" SSH_USER="deploy" SSH_KEY_PATH="$HOME/.ssh/production" DRY_RUN=false FORCE=false SKIP_WIZARD=false SKIP_TESTS=false SKIP_BACKUP=false VERBOSE=false AUTO_YES=false # State tracking STEP_COUNT=0 TOTAL_STEPS=12 # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' WHITE='\033[1;37m' NC='\033[0m' # Enhanced logging log() { echo -e "${GREEN}[$(date +'%H:%M:%S')] ✅ $1${NC}"; } warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠️ $1${NC}"; } error() { echo -e "${RED}[$(date +'%H:%M:%S')] ❌ $1${NC}"; } info() { echo -e "${BLUE}[$(date +'%H:%M:%S')] ℹ️ $1${NC}"; } success() { echo -e "${WHITE}[$(date +'%H:%M:%S')] 🎉 $1${NC}"; } debug() { [[ "$VERBOSE" == "true" ]] && echo -e "${CYAN}[$(date +'%H:%M:%S')] 🔍 DEBUG: $1${NC}" || true; } # Progress indicator step() { ((STEP_COUNT++)) local progress=$((STEP_COUNT * 100 / TOTAL_STEPS)) local bars=$((progress / 5)) local spaces=$((20 - bars)) printf "\n${PURPLE}[Step %d/%d] ${WHITE}%s${NC}\n" "$STEP_COUNT" "$TOTAL_STEPS" "$1" printf "${CYAN}Progress: [${GREEN}" printf "█%.0s" $(seq 1 $bars) printf "${CYAN}" printf "░%.0s" $(seq 1 $spaces) printf "${CYAN}] %d%%${NC}\n\n" "$progress" } # Usage information show_usage() { cat << EOF ${WHITE}One-Command Production Setup${NC} ${CYAN}Custom PHP Framework - Complete Automated Deployment${NC} ${WHITE}Usage:${NC} $0 [options] ${WHITE}Options:${NC} ${YELLOW}--server IP${NC} Server IP address (default: ${SERVER_IP}) ${YELLOW}--domain DOMAIN${NC} Domain name (default: ${DOMAIN}) ${YELLOW}--email EMAIL${NC} Contact email (default: ${EMAIL}) ${YELLOW}--ssh-user USER${NC} SSH username (default: ${SSH_USER}) ${YELLOW}--ssh-key PATH${NC} SSH private key path (default: ${SSH_KEY_PATH}) ${YELLOW}--dry-run${NC} Show what would be done without executing ${YELLOW}--force${NC} Skip confirmations and force execution ${YELLOW}--skip-wizard${NC} Skip interactive wizard (use CLI args only) ${YELLOW}--skip-tests${NC} Skip running tests before deployment ${YELLOW}--skip-backup${NC} Skip database backup ${YELLOW}--auto-yes${NC} Automatically answer yes to all prompts ${YELLOW}--verbose${NC} Enable verbose output ${YELLOW}-h, --help${NC} Show this help message ${WHITE}What this script does:${NC} 1. ✅ System prerequisites validation 2. ✅ SSH connectivity and server preparation 3. ✅ Secure credential generation 4. ✅ Environment configuration from templates 5. ✅ Docker and dependency installation 6. ✅ Ansible inventory configuration 7. ✅ Infrastructure deployment (base security, Docker, Nginx) 8. ✅ SSL certificate setup with Let's Encrypt 9. ✅ Application deployment with Docker Compose 10. ✅ Database migration and setup 11. ✅ Comprehensive health checks 12. ✅ Production readiness validation ${WHITE}Examples:${NC} ${CYAN}$0${NC} # Interactive production setup ${CYAN}$0 --auto-yes --verbose${NC} # Automated setup with detailed output ${CYAN}$0 --server 1.2.3.4 --skip-wizard${NC} # Custom server, no wizard ${CYAN}$0 --dry-run --verbose${NC} # Preview all operations ${WHITE}Safety Features:${NC} • Production confirmation required unless --force • Comprehensive rollback on failure • Automatic backups before changes • Health validation at each step • Complete audit trail EOF } # Parse command line arguments parse_arguments() { while [[ $# -gt 0 ]]; do case $1 in --server) SERVER_IP="$2" shift 2 ;; --domain) DOMAIN="$2" shift 2 ;; --email) EMAIL="$2" shift 2 ;; --ssh-user) SSH_USER="$2" shift 2 ;; --ssh-key) SSH_KEY_PATH="$2" shift 2 ;; --dry-run) DRY_RUN=true shift ;; --force) FORCE=true shift ;; --skip-wizard) SKIP_WIZARD=true shift ;; --skip-tests) SKIP_TESTS=true shift ;; --skip-backup) SKIP_BACKUP=true shift ;; --auto-yes) AUTO_YES=true shift ;; --verbose) VERBOSE=true shift ;; -h|--help) show_usage exit 0 ;; *) error "Unknown argument: $1" show_usage exit 1 ;; esac done } # Production confirmation confirm_production_setup() { if [[ "$FORCE" == "true" ]] || [[ "$AUTO_YES" == "true" ]]; then return 0 fi cat << EOF ${RED}⚠️ PRODUCTION DEPLOYMENT WARNING ⚠️${NC} You are about to set up and deploy to a PRODUCTION server: • Server: ${WHITE}${SSH_USER}@${SERVER_IP}${NC} • Domain: ${WHITE}${DOMAIN}${NC} • This will install software and modify server configuration • SSL certificates will be requested from Let's Encrypt • The application will be accessible on the internet EOF if [[ "$DRY_RUN" == "true" ]]; then echo -e "${BLUE}This is a DRY RUN - no actual changes will be made.${NC}\n" else echo -e "${RED}This is a LIVE SETUP - changes will be applied immediately!${NC}\n" fi printf "${CYAN}Are you absolutely sure you want to continue? [y/N]: ${NC}" read -r response if [[ ! $response =~ ^[Yy]$ ]]; then log "Production setup cancelled by user" exit 0 fi # Double confirmation for live deployment if [[ "$DRY_RUN" != "true" ]]; then printf "${RED}Final confirmation - Type 'DEPLOY PRODUCTION' to continue: ${NC}" read -r final_response if [[ "$final_response" != "DEPLOY PRODUCTION" ]]; then log "Production setup cancelled - final confirmation not received" exit 0 fi fi } # Step 1: Prerequisites validation step_validate_prerequisites() { step "Validating Prerequisites" local missing_tools=() # Check required tools local tools=("docker" "docker-compose" "ansible-playbook" "ssh" "openssl") for tool in "${tools[@]}"; do if ! command -v "$tool" >/dev/null 2>&1; then missing_tools+=("$tool") else debug "✓ $tool found" fi done if [[ ${#missing_tools[@]} -gt 0 ]]; then error "Missing required tools: ${missing_tools[*]}" info "Install missing tools and run again" exit 1 fi # Check project structure local required_files=( "docker-compose.yml" "deployment/deploy.sh" "deployment/applications/docker-compose.production.yml" "deployment/infrastructure/site.yml" "deployment/applications/environments/production.env.template" ) for file in "${required_files[@]}"; do if [[ ! -f "${PROJECT_ROOT}/${file}" ]]; then error "Required file missing: $file" exit 1 else debug "✓ $file found" fi done success "All prerequisites validated" } # Step 2: SSH connectivity test step_test_ssh_connectivity() { step "Testing SSH Connectivity" info "Testing SSH connection to ${SSH_USER}@${SERVER_IP}" if [[ ! -f "$SSH_KEY_PATH" ]]; then error "SSH key not found: $SSH_KEY_PATH" info "Generate SSH key with: ssh-keygen -t ed25519 -f $SSH_KEY_PATH" exit 1 fi if ssh -i "$SSH_KEY_PATH" -o ConnectTimeout=10 -o BatchMode=yes \ "${SSH_USER}@${SERVER_IP}" "echo 'SSH connection successful'" >/dev/null 2>&1; then success "SSH connectivity test passed" # Get server info local server_info server_info=$(ssh -i "$SSH_KEY_PATH" "${SSH_USER}@${SERVER_IP}" \ "uname -a && free -h && df -h / | tail -n +2") debug "Server info: $server_info" else error "SSH connection failed to ${SSH_USER}@${SERVER_IP}" cat << EOF ${YELLOW}Troubleshooting SSH Connection:${NC} 1. Verify SSH key is correct: ${SSH_KEY_PATH} 2. Check if key is added to server: ssh-copy-id -i ${SSH_KEY_PATH}.pub ${SSH_USER}@${SERVER_IP} 3. Test manual connection: ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${SERVER_IP} 4. Check firewall allows SSH (port 22) EOF exit 1 fi } # Step 3: Generate secure credentials step_generate_credentials() { step "Generating Secure Credentials" info "Generating cryptographically secure passwords and keys..." # Create secure credentials directory local creds_dir="${DEPLOYMENT_DIR}/.credentials" mkdir -p "$creds_dir" chmod 700 "$creds_dir" local creds_file="${creds_dir}/production.env" cat > "$creds_file" << EOF # Generated $(date) # Custom PHP Framework Production Credentials DB_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25) DB_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25) REDIS_PASSWORD=$(openssl rand -base64 32 | tr -d "=+/" | cut -c1-25) APP_KEY=$(openssl rand -base64 32) GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 16 | tr -d "=+/" | cut -c1-16) SHOPIFY_WEBHOOK_SECRET=$(openssl rand -hex 32) EOF chmod 600 "$creds_file" success "Secure credentials generated: $creds_file" } # Step 4: Create production environment step_create_production_environment() { step "Creating Production Environment" local env_file="${DEPLOYMENT_DIR}/applications/environments/.env.production" local template_file="${env_file}.template" local creds_file="${DEPLOYMENT_DIR}/.credentials/production.env" if [[ ! -f "$template_file" ]]; then error "Production template not found: $template_file" exit 1 fi info "Creating production environment from template..." if [[ "$DRY_RUN" != "true" ]]; then # Copy template cp "$template_file" "$env_file" # Apply domain and email sed -i "s|DOMAIN_NAME=.*|DOMAIN_NAME=${DOMAIN}|g" "$env_file" sed -i "s|MAIL_FROM_ADDRESS=.*|MAIL_FROM_ADDRESS=${EMAIL}|g" "$env_file" # Apply generated credentials source "$creds_file" sed -i "s|DB_PASSWORD=\*\*\* REQUIRED \*\*\*|DB_PASSWORD=${DB_PASSWORD}|g" "$env_file" sed -i "s|DB_ROOT_PASSWORD=\*\*\* REQUIRED \*\*\*|DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}|g" "$env_file" sed -i "s|REDIS_PASSWORD=\*\*\* REQUIRED \*\*\*|REDIS_PASSWORD=${REDIS_PASSWORD}|g" "$env_file" sed -i "s|APP_KEY=\*\*\* REQUIRED \*\*\*|APP_KEY=${APP_KEY}|g" "$env_file" sed -i "s|GRAFANA_ADMIN_PASSWORD=\*\*\* REQUIRED \*\*\*|GRAFANA_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}|g" "$env_file" sed -i "s|SHOPIFY_WEBHOOK_SECRET=\*\*\* REQUIRED \*\*\*|SHOPIFY_WEBHOOK_SECRET=${SHOPIFY_WEBHOOK_SECRET}|g" "$env_file" # Set production-specific settings sed -i 's|APP_DEBUG=.*|APP_DEBUG=false|g' "$env_file" sed -i 's|LOG_LEVEL=.*|LOG_LEVEL=warning|g' "$env_file" sed -i 's|APP_ENV=.*|APP_ENV=production|g' "$env_file" chmod 600 "$env_file" success "Production environment created: $env_file" else debug "DRY RUN: Would create production environment file" fi } # Step 5: Configure Ansible inventory step_configure_ansible_inventory() { step "Configuring Ansible Inventory" local inventory_file="${DEPLOYMENT_DIR}/infrastructure/inventories/production/hosts.yml" info "Updating Ansible inventory for production server..." if [[ "$DRY_RUN" != "true" ]]; then # Backup existing inventory if [[ -f "$inventory_file" ]]; then cp "$inventory_file" "${inventory_file}.backup.$(date +%Y%m%d_%H%M%S)" fi # Create/update inventory mkdir -p "$(dirname "$inventory_file")" cat > "$inventory_file" << EOF # Production Inventory for Custom PHP Framework # Generated: $(date) all: children: production: hosts: production-server: ansible_host: ${SERVER_IP} ansible_user: ${SSH_USER} ansible_port: 22 ansible_ssh_private_key_file: ${SSH_KEY_PATH} ansible_ssh_common_args: '-o StrictHostKeyChecking=no' # Domain configuration domain_name: ${DOMAIN} ssl_email: ${EMAIL} # Application configuration app_env: production php_version: "8.4" # Security configuration enable_firewall: true enable_fail2ban: true ssh_hardening: true # Performance configuration enable_opcache: true enable_redis: true # Monitoring enable_monitoring: true enable_health_checks: true EOF success "Ansible inventory configured: $inventory_file" else debug "DRY RUN: Would configure Ansible inventory" fi } # Step 6: Run pre-deployment tests step_run_tests() { if [[ "$SKIP_TESTS" == "true" ]]; then step "Skipping Tests (as requested)" return 0 fi step "Running Pre-Deployment Tests" cd "$PROJECT_ROOT" info "Running PHP tests..." if [[ "$DRY_RUN" != "true" ]]; then if [[ -f "vendor/bin/pest" ]]; then ./vendor/bin/pest --bail || { error "PHP tests failed" exit 1 } else warn "No PHP tests found" fi info "Running code style checks..." if [[ -f "composer.json" ]]; then composer cs || { error "Code style checks failed" exit 1 } fi else debug "DRY RUN: Would run tests and code style checks" fi success "All pre-deployment tests passed" } # Step 7: Deploy infrastructure step_deploy_infrastructure() { step "Deploying Infrastructure" cd "${DEPLOYMENT_DIR}/infrastructure" local inventory="inventories/production/hosts.yml" local playbook="site.yml" info "Deploying infrastructure with Ansible..." info "This includes: base security, Docker, Nginx, SSL setup" local ansible_cmd="ansible-playbook -i $inventory $playbook" if [[ "$DRY_RUN" == "true" ]]; then ansible_cmd="$ansible_cmd --check --diff" fi if [[ "$VERBOSE" == "true" ]]; then ansible_cmd="$ansible_cmd -vv" fi debug "Running: $ansible_cmd" if [[ "$DRY_RUN" != "true" ]]; then $ansible_cmd || { error "Infrastructure deployment failed" exit 1 } success "Infrastructure deployment completed" else debug "DRY RUN: Would deploy infrastructure" success "Infrastructure deployment dry run completed" fi } # Step 8: Setup SSL certificates step_setup_ssl() { step "Setting up SSL Certificates" info "Configuring Let's Encrypt SSL for ${DOMAIN}..." if [[ "$DRY_RUN" != "true" ]]; then # SSL setup is handled by Ansible, verify it worked info "Verifying SSL certificate installation..." if ssh -i "$SSH_KEY_PATH" "${SSH_USER}@${SERVER_IP}" \ "test -f /etc/letsencrypt/live/${DOMAIN}/fullchain.pem"; then success "SSL certificate verified" else warn "SSL certificate not found, may need manual setup" fi else debug "DRY RUN: Would setup SSL certificates" fi success "SSL configuration completed" } # Step 9: Deploy application step_deploy_application() { step "Deploying Application" cd "$PROJECT_ROOT" info "Building and deploying Custom PHP Framework application..." local deploy_cmd="./deployment/deploy.sh production --application-only" if [[ "$DRY_RUN" == "true" ]]; then deploy_cmd="$deploy_cmd --dry-run" fi if [[ "$SKIP_BACKUP" == "true" ]]; then deploy_cmd="$deploy_cmd --skip-backup" fi if [[ "$VERBOSE" == "true" ]]; then deploy_cmd="$deploy_cmd --verbose" fi debug "Running: $deploy_cmd" $deploy_cmd || { error "Application deployment failed" exit 1 } success "Application deployment completed" } # Step 10: Run database migrations step_run_migrations() { step "Running Database Migrations" info "Applying database schema migrations..." if [[ "$DRY_RUN" != "true" ]]; then # Run migrations via SSH ssh -i "$SSH_KEY_PATH" "${SSH_USER}@${SERVER_IP}" \ "cd /var/www/html && docker-compose exec -T php php console.php db:migrate" || { error "Database migration failed" exit 1 } success "Database migrations completed" else debug "DRY RUN: Would run database migrations" fi } # Step 11: Comprehensive health checks step_health_checks() { step "Running Comprehensive Health Checks" info "Performing production readiness validation..." # Test HTTPS connectivity info "Testing HTTPS connectivity..." if [[ "$DRY_RUN" != "true" ]]; then if curl -sSf "https://${DOMAIN}" >/dev/null 2>&1; then success "✓ HTTPS connectivity working" else warn "HTTPS connectivity test failed" fi # Test API endpoints info "Testing API health endpoint..." if curl -sSf "https://${DOMAIN}/api/health" >/dev/null 2>&1; then success "✓ API health check passed" else warn "API health check failed" fi # Check Docker containers info "Verifying Docker containers..." local containers_status containers_status=$(ssh -i "$SSH_KEY_PATH" "${SSH_USER}@${SERVER_IP}" \ "docker-compose ps --services --filter status=running" | wc -l) if [[ $containers_status -gt 0 ]]; then success "✓ Docker containers running ($containers_status active)" else error "No Docker containers running" fi else debug "DRY RUN: Would run comprehensive health checks" fi success "Health checks completed" } # Step 12: Final validation and summary step_final_summary() { step "Final Validation and Summary" cat << EOF ${WHITE}🎉 PRODUCTION SETUP COMPLETED SUCCESSFULLY! 🎉${NC} ${CYAN}Deployment Summary:${NC} • Environment: ${WHITE}Production${NC} • Domain: ${WHITE}https://${DOMAIN}${NC} • Server: ${WHITE}${SSH_USER}@${SERVER_IP}${NC} • PHP Version: ${WHITE}8.4${NC} • Framework: ${WHITE}Custom PHP Framework${NC} ${CYAN}What was deployed:${NC} • ✅ Infrastructure (Ansible) - Base security hardening with fail2ban and firewall - Docker runtime environment optimized for PHP 8.4 - Nginx reverse proxy with SSL/TLS - System monitoring and health checks • ✅ Application (Docker Compose) - PHP 8.4 application container with OPcache - MySQL database with optimized configuration - Redis for caching and sessions - Frontend assets built and optimized - Comprehensive logging and monitoring ${CYAN}Security Features Enabled:${NC} • 🔒 SSL/TLS encryption with Let's Encrypt • 🛡️ Web Application Firewall (WAF) • 🔐 SSH hardening and key-based authentication • 🚫 Fail2ban intrusion prevention • 🔍 System monitoring and alerting • 📊 Performance metrics collection ${CYAN}Files Created:${NC} • ${YELLOW}deployment/applications/environments/.env.production${NC} • ${YELLOW}deployment/infrastructure/inventories/production/hosts.yml${NC} • ${YELLOW}deployment/.credentials/production.env${NC} EOF if [[ "$DRY_RUN" == "true" ]]; then cat << EOF ${BLUE}Note: This was a dry run. No actual changes were made.${NC} ${BLUE}Remove the --dry-run flag to perform the actual setup.${NC} EOF else cat << EOF ${GREEN}🌟 Your Custom PHP Framework is now live at: https://${DOMAIN}${NC} ${CYAN}Next Steps:${NC} 1. Test all application functionality 2. Review monitoring dashboards (if enabled) 3. Set up automated backups 4. Configure domain DNS if needed 5. Review security logs and metrics ${CYAN}Useful Commands:${NC} • Monitor logs: ${YELLOW}ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${SERVER_IP} 'docker-compose logs -f'${NC} • Check status: ${YELLOW}./deployment/deploy.sh production --dry-run${NC} • Update deployment: ${YELLOW}./deployment/deploy.sh production${NC} ${CYAN}Important Security Notes:${NC} • Store credentials securely: ${YELLOW}deployment/.credentials/production.env${NC} • Regular security updates are automated • Monitor fail2ban logs for intrusion attempts • SSL certificates auto-renew via Let's Encrypt EOF fi success "Production setup completed successfully!" } # Error handling and rollback cleanup() { local exit_code=$? if [[ $exit_code -ne 0 ]]; then error "Production setup failed with exit code: $exit_code" cat << EOF ${RED}💥 PRODUCTION SETUP FAILED${NC} ${YELLOW}Failure occurred at step ${STEP_COUNT}/${TOTAL_STEPS}${NC} ${CYAN}Troubleshooting:${NC} 1. Check the error messages above for specific issues 2. Verify SSH connectivity: ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${SERVER_IP} 3. Check server logs: ssh -i ${SSH_KEY_PATH} ${SSH_USER}@${SERVER_IP} 'journalctl -xe' 4. Review configuration files for issues 5. Try running with --verbose for detailed output ${CYAN}Rollback Options:${NC} • Infrastructure: Run Ansible with --tags rollback • Application: Restore from backup (if available) • Full reset: Reinstall server OS and start over ${CYAN}Get Help:${NC} • Check deployment documentation • Review logs in deployment/infrastructure/logs/ • Use --dry-run to test before retrying EOF fi } trap cleanup EXIT # Main execution main() { # Check if running with wizard unless explicitly skipped if [[ "$SKIP_WIZARD" != "true" ]] && [[ "$AUTO_YES" != "true" ]] && [[ -t 0 ]]; then info "Starting setup wizard for interactive configuration..." source "${SCRIPT_DIR}/setup-wizard.sh" return fi cat << 'EOF' ██████╗ ██████╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗████████╗██╗ ██████╗ ███╗ ██╗ ██╔══██╗██╔══██╗██╔═══██╗██╔══██╗██║ ██║██╔════╝╚══██╔══╝██║██╔═══██╗████╗ ██║ ██████╔╝██████╔╝██║ ██║██║ ██║██║ ██║██║ ██║ ██║██║ ██║██╔██╗ ██║ ██╔═══╝ ██╔══██╗██║ ██║██║ ██║██║ ██║██║ ██║ ██║██║ ██║██║╚██╗██║ ██║ ██║ ██║╚██████╔╝██████╔╝╚██████╔╝╚██████╗ ██║ ██║╚██████╔╝██║ ╚████║ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ EOF info "Custom PHP Framework - One-Command Production Setup" info "Domain: $DOMAIN | Server: $SERVER_IP | Environment: Production" if [[ "$DRY_RUN" == "true" ]]; then warn "DRY RUN MODE - No actual changes will be made" fi # Confirm production setup confirm_production_setup # Execute all setup steps step_validate_prerequisites step_test_ssh_connectivity step_generate_credentials step_create_production_environment step_configure_ansible_inventory step_run_tests step_deploy_infrastructure step_setup_ssl step_deploy_application step_run_migrations step_health_checks step_final_summary } # Execute if run directly if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then parse_arguments "$@" main fi