name: Deploy to Production on: push: branches: - main - production workflow_dispatch: inputs: force_rebuild: description: 'Force rebuild Docker image' required: false default: 'false' skip_backup: description: 'Skip database backup (not recommended)' required: false default: 'false' env: REGISTRY: localhost:5000 IMAGE_NAME: framework IMAGE_TAG: latest COMPOSE_PROJECT_NAME: framework-production jobs: build-and-deploy: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build Docker image run: | echo "Building Docker image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }}" docker build \ --file docker/php/Dockerfile \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ --build-arg ENV=production \ --build-arg COMPOSER_INSTALL_FLAGS="--no-dev --optimize-autoloader --no-interaction" \ . - name: Push image to private registry run: | echo "Pushing image to registry..." docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ env.IMAGE_TAG }} - name: Prepare deployment files run: | echo "Preparing deployment files..." mkdir -p deployment-production cp docker-compose.base.yml deployment-production/ cp docker-compose.prod.yml deployment-production/ cp -r docker deployment-production/ # Create deployment script cat > deployment-production/deploy.sh << 'EOF' #!/bin/bash set -e echo "==================================================" echo "Starting Production Deployment" 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 "==================================================" EOF chmod +x deployment-production/deploy.sh - name: Deploy to production server uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | # Create deployment directory mkdir -p /opt/framework-production cd /opt/framework-production # 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_* | tail -n +11 | xargs rm -rf fi # Create new deployment directory mkdir -p current cd current - name: Copy deployment files uses: appleboy/scp-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} source: "deployment-production/*" target: "/opt/framework-production/current/" strip_components: 1 - name: Execute deployment uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | cd /opt/framework-production/current # Set skip backup flag if provided export SKIP_BACKUP="${{ github.event.inputs.skip_backup || 'false' }}" # Execute deployment script ./deploy.sh - name: Health check uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | cd /opt/framework-production/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 # 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 # Test HTTP endpoint (via Traefik) echo "Testing production endpoint..." curl -f -k https://michaelschiemer.de/health || { echo "⚠️ Health check endpoint failed" exit 1 } # 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 "" echo "✅ All health checks passed!" - name: Smoke tests uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | 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" - name: Rollback on failure if: failure() uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | cd /opt/framework-production 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 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" exit 1 fi - name: Notify deployment status if: always() uses: appleboy/ssh-action@master with: host: ${{ secrets.PRODUCTION_HOST }} username: ${{ secrets.PRODUCTION_USER }} key: ${{ secrets.PRODUCTION_SSH_KEY }} port: ${{ secrets.PRODUCTION_SSH_PORT || 22 }} script: | if [ "${{ job.status }}" == "success" ]; then echo "✅ Production deployment successful" echo "URL: https://michaelschiemer.de" echo "Deployed at: $(date)" # Log deployment echo "$(date) - Deployment SUCCESS - Commit: ${{ github.sha }}" >> /opt/framework-production/deployment.log else echo "❌ Production deployment failed - rollback executed" # Log deployment failure echo "$(date) - Deployment FAILED - Commit: ${{ github.sha }}" >> /opt/framework-production/deployment.log # Send alert (placeholder - implement actual alerting) echo "⚠️ ALERT: Production deployment failed. Manual intervention may be required." fi - name: Clean up build artifacts if: always() run: | echo "Cleaning up deployment artifacts..." rm -rf deployment-production echo "✅ Cleanup complete"