#!/bin/bash set -e # ============================================================================ # Production Deployment Script # ============================================================================ # Based on .gitea/workflows/deploy-production.yml # Deploys to production environment at michaelschiemer.de # ============================================================================ # Color output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Configuration REGISTRY="${REGISTRY:-localhost:5000}" IMAGE_NAME="${IMAGE_NAME:-framework}" IMAGE_TAG="${IMAGE_TAG:-latest}" COMPOSE_PROJECT_NAME="framework-production" # Remote server configuration (from environment or defaults) REMOTE_USER="${PRODUCTION_USER:-deploy}" REMOTE_HOST="${PRODUCTION_HOST:-michaelschiemer.de}" REMOTE_PORT="${PRODUCTION_SSH_PORT:-22}" REMOTE_PATH="/opt/framework-production" # Deployment options SKIP_BACKUP="${SKIP_BACKUP:-false}" FORCE_REBUILD="${FORCE_REBUILD:-false}" # Validation if [ -z "$REMOTE_HOST" ]; then echo -e "${RED}❌ Error: PRODUCTION_HOST environment variable is required${NC}" echo "Export PRODUCTION_HOST before running this script:" echo " export PRODUCTION_HOST=michaelschiemer.de" exit 1 fi echo "==================================================" echo "🚀 Starting Production Deployment" echo "==================================================" echo "Registry: ${REGISTRY}" echo "Image: ${IMAGE_NAME}:${IMAGE_TAG}" echo "Remote: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT}" echo "Path: ${REMOTE_PATH}" echo "Skip Backup: ${SKIP_BACKUP}" echo "" # Step 1: Build Docker image echo -e "${GREEN}[1/8] Building Docker image...${NC}" docker build \ --file docker/php/Dockerfile \ --tag "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" \ --build-arg ENV=production \ --build-arg COMPOSER_INSTALL_FLAGS="--no-dev --optimize-autoloader --no-interaction" \ . if [ $? -ne 0 ]; then echo -e "${RED}❌ Docker build failed${NC}" exit 1 fi # Step 2: Push image to registry echo -e "${GREEN}[2/8] Pushing image to registry...${NC}" docker push "${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" if [ $? -ne 0 ]; then echo -e "${RED}❌ Docker push failed${NC}" exit 1 fi # Step 3: Prepare deployment files echo -e "${GREEN}[3/8] Preparing deployment files...${NC}" rm -rf deployment-production-tmp mkdir -p deployment-production-tmp cp docker-compose.base.yml deployment-production-tmp/ cp docker-compose.prod.yml deployment-production-tmp/ cp -r docker deployment-production-tmp/ # Create deployment script cat > deployment-production-tmp/deploy.sh << 'DEPLOY_SCRIPT' #!/bin/bash set -e echo "==================================================" echo "Starting Production Deployment on Server" echo "==================================================" echo "" # Database backup (unless explicitly skipped) if [ "${SKIP_BACKUP}" != "true" ]; then echo "[0/6] Creating database backup..." BACKUP_FILE="backup_$(date +%Y%m%d_%H%M%S).sql" docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \ php console.php db:backup --output="/var/www/html/storage/backups/${BACKUP_FILE}" || { echo "⚠️ Database backup failed - deployment aborted" exit 1 } echo "✅ Database backup created: ${BACKUP_FILE}" else echo "⚠️ Database backup skipped (not recommended for production)" fi # Pull latest images echo "[1/6] Pulling latest Docker images..." docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml pull # Stop existing containers gracefully echo "[2/6] Stopping existing containers (graceful shutdown)..." docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml stop # Start new containers echo "[3/6] Starting new containers..." docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d # Wait for services to be healthy (longer timeout for production) echo "[4/6] Waiting for services to be healthy..." sleep 30 # Run database migrations echo "[5/6] Running database migrations..." docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app \ php console.php db:migrate --force || { echo "⚠️ Database migration failed" exit 1 } # Verify deployment echo "[6/6] Verifying deployment..." docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps # Cleanup old containers echo "Cleaning up old containers..." docker system prune -f echo "" echo "==================================================" echo "✅ Production Deployment Complete" echo "==================================================" DEPLOY_SCRIPT chmod +x deployment-production-tmp/deploy.sh # Step 4: Create remote directory and backup echo -e "${GREEN}[4/8] Creating remote directory and backup...${NC}" ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" " mkdir -p ${REMOTE_PATH} cd ${REMOTE_PATH} # Backup current deployment if [ -d 'current' ]; then echo 'Backing up current deployment...' timestamp=\$(date +%Y%m%d_%H%M%S) mv current backup_\${timestamp} # Keep only last 10 backups for production ls -dt backup_* 2>/dev/null | tail -n +11 | xargs rm -rf 2>/dev/null || true echo 'Backup created: backup_'\${timestamp} fi # Create new deployment directory mkdir -p current " if [ $? -ne 0 ]; then echo -e "${RED}❌ SSH connection or remote directory creation failed${NC}" rm -rf deployment-production-tmp exit 1 fi # Step 5: Copy deployment files to server echo -e "${GREEN}[5/8] Copying deployment files to server...${NC}" scp -P "${REMOTE_PORT}" -r deployment-production-tmp/* "${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PATH}/current/" if [ $? -ne 0 ]; then echo -e "${RED}❌ SCP file transfer failed${NC}" rm -rf deployment-production-tmp exit 1 fi # Cleanup local temp directory rm -rf deployment-production-tmp # Step 6: Execute deployment on server echo -e "${GREEN}[6/8] Executing deployment on server...${NC}" ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" " cd ${REMOTE_PATH}/current export SKIP_BACKUP=${SKIP_BACKUP} ./deploy.sh " DEPLOY_EXIT_CODE=$? if [ $DEPLOY_EXIT_CODE -ne 0 ]; then echo -e "${RED}❌ Deployment execution failed${NC}" echo -e "${YELLOW}⚠️ Attempting automatic rollback...${NC}" ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" " cd ${REMOTE_PATH} if [ -d \"\$(ls -dt backup_* 2>/dev/null | head -n1)\" ]; then echo '🚨 Rolling back to previous deployment...' latest_backup=\$(ls -dt backup_* | head -n1) # Stop current broken deployment cd current docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml down 2>/dev/null || true cd .. # Restore backup rm -rf current cp -r \"\$latest_backup\" current # Start restored deployment cd current docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml up -d # Wait for services sleep 30 # Verify rollback docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps echo '✅ Rollback complete - previous version restored' else echo '❌ No backup available for rollback' echo '⚠️ MANUAL INTERVENTION REQUIRED' fi " exit 1 fi # Step 7: Health check echo -e "${GREEN}[7/8] Performing health checks...${NC}" ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" " cd ${REMOTE_PATH}/current # Wait for services to be fully ready (longer for production) echo 'Waiting 60 seconds for services to initialize...' sleep 60 # Check container status echo 'Checking container status...' docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml ps # Check service health echo 'Checking service health...' docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app php -v || echo 'PHP health check skipped' # Check PHP-FPM is running echo 'Checking PHP-FPM process...' docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-app pgrep php-fpm || echo 'PHP-FPM check skipped' # Check Redis connection echo 'Checking Redis connection...' docker-compose -f docker-compose.base.yml -f docker-compose.prod.yml exec -T production-redis redis-cli ping || echo 'Redis check skipped' echo '' echo '✅ All health checks passed!' " if [ $? -ne 0 ]; then echo -e "${YELLOW}⚠️ Health check had issues but deployment may still be functional${NC}" fi # Step 8: Smoke tests echo -e "${GREEN}[8/8] Running smoke tests...${NC}" ssh -p "${REMOTE_PORT}" "${REMOTE_USER}@${REMOTE_HOST}" " echo 'Running production smoke tests...' # Test main page curl -f -k https://michaelschiemer.de/ > /dev/null 2>&1 && echo '✅ Main page accessible' || { echo '❌ Main page failed' exit 1 } # Test API health curl -f -k https://michaelschiemer.de/api/health > /dev/null 2>&1 && echo '✅ API health check passed' || { echo '❌ API health check failed' exit 1 } echo '✅ Smoke tests completed successfully' " if [ $? -ne 0 ]; then echo -e "${RED}❌ Smoke tests failed${NC}" echo -e "${YELLOW}⚠️ Deployment completed but smoke tests did not pass${NC}" exit 1 fi echo "" echo "==================================================" echo -e "${GREEN}✅ Production Deployment Successful${NC}" echo "==================================================" echo "URL: https://michaelschiemer.de" echo "Deployed at: $(date)" echo "" echo "Next steps:" echo " - Monitor logs: ssh ${REMOTE_USER}@${REMOTE_HOST} 'cd ${REMOTE_PATH}/current && docker-compose logs -f'" echo " - Check status: ssh ${REMOTE_USER}@${REMOTE_HOST} 'cd ${REMOTE_PATH}/current && docker-compose ps'" echo ""