Jump to content

Full CI/CD Pipeline for PHP + MySQL + HTML Application


Gani
 Share

Recommended Posts

  • Moderators

📌 Complete Architecture

 
Quote

PHP Application (Laravel/CodeIgniter/Custom PHP)
├── Frontend: HTML, CSS, JavaScript
├── Backend: PHP
├── Database: MySQL
└── CI/CD: GitHub Actions + Deployment Scripts

Step 1: Project Structure

 

1.1 Sample Project Layout

Quote

my-php-app/
├── public/
│   ├── index.php
│   ├── css/
│   ├── js/
│   └── images/
├── src/
│   ├── controllers/
│   ├── models/
│   ├── views/
│   └── lib/
├── database/
│   ├── migrations/
│   └── seeds/
├── tests/
│   ├── unit/
│   └── integration/
├── .env.example
├── composer.json
├── .htaccess
├── .github/
│   └── workflows/
│       └── php-ci-cd.yml
├── scripts/
│   ├── deploy.sh
│   └── database.sh
└── README.md

1.2 Sample Files

public/index.php:

Quote

<?php
require_once __DIR__ . '/../vendor/autoload.php';

// Database connection
$host = getenv('DB_HOST') ?: 'localhost';
$dbname = getenv('DB_NAME') ?: 'myapp';
$username = getenv('DB_USER') ?: 'root';
$password = getenv('DB_PASSWORD') ?: '';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname", $username, $password);
    echo "Database connected successfully!";
} catch (PDOException $e) {
    echo "Connection failed: " . $e->getMessage();
}

// Your application logic here
?>

composer.json:

 
Quote

{
    "name": "yourname/my-php-app",
    "description": "PHP Application with MySQL",
    "require": {
        "php": ">=7.4",
        "ext-pdo": "*"
    },
    "require-dev": {
        "phpunit/phpunit": "^9.0",
        "squizlabs/php_codesniffer": "^3.0"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "scripts": {
        "test": "phpunit",
        "lint": "phpcs --standard=PSR12 src/ tests/",
        "fix": "phpcbf --standard=PSR12 src/ tests/"
    }
}

Step 2: Complete CI/CD Pipeline with GitHub Actions

2.1 Full Pipeline Configuration

 

Create .github/workflows/php-ci-cd.yml:

 
Quote

name: PHP CI/CD Pipeline

on:
  push:
    branches: [ main, master, develop ]
  pull_request:
    branches: [ main, master ]

jobs:
  # Job 1: Continuous Integration (Testing)
  ci:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        extensions: mbstring, xml, mysql, pdo, pdo_mysql
        coverage: xdebug
        
    - name: Validate composer.json
      run: composer validate
      
    - name: Install dependencies
      run: composer install --prefer-dist --no-progress --no-suggest
      
    - name: Copy environment file
      run: cp .env.example .env
      
    - name: Setup database
      run: |
        mysql --host 127.0.0.1 --port 3306 -uroot -proot -e "CREATE DATABASE IF NOT EXISTS test_db;"
        mysql --host 127.0.0.1 --port 3306 -uroot -proot -e "CREATE DATABASE IF NOT EXISTS app_db;"
      
    - name: Run database migrations
      run: |
        php scripts/database.sh migrate test
      
    - name: Run PHP Code Sniffer
      run: composer lint
      
    - name: Run PHPUnit tests
      run: composer test
      env:
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: test_db
        DB_USERNAME: root
        DB_PASSWORD: root
        
    - name: Run security check
      uses: symfonycorp/security-checker-action@v5
        
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: tests/report/
        
  # Job 2: Build and Package
  build:
    runs-on: ubuntu-latest
    needs: ci
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop'
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.1'
        
    - name: Install dependencies (no dev)
      run: composer install --no-dev --prefer-dist --optimize-autoloader
        
    - name: Create deployment package
      run: |
        mkdir -p deployment_package
        # Copy necessary files (exclude development files)
        rsync -av --exclude='.git' \
                     --exclude='.github' \
                     --exclude='tests' \
                     --exclude='*.md' \
                     --exclude='.env' \
                     --exclude='composer.json' \
                     --exclude='composer.lock' \
                     --exclude='phpunit.xml' \
                     . deployment_package/
        
    - name: Upload deployment package
      uses: actions/upload-artifact@v3
      with:
        name: php-deployment-package
        path: deployment_package/
        
  # Job 3: Deploy to Staging
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    
    steps:
    - name: Download deployment package
      uses: actions/download-artifact@v3
      with:
        name: php-deployment-package
        
    - name: Deploy to staging server
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.STAGING_HOST }}
        username: ${{ secrets.STAGING_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        source: "*"
        target: "/var/www/staging.myapp.com"
        strip_components: 1
        
    - name: Run deployment script on staging
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.STAGING_HOST }}
        username: ${{ secrets.STAGING_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/staging.myapp.com
          chmod +x scripts/deploy.sh
          ./scripts/deploy.sh staging
          
    - name: Run smoke test on staging
      uses: jawnsy/action-http-request@v1
      with:
        url: "https://staging.myapp.com"
        method: "GET"
        
  # Job 4: Deploy to Production (with approval)
  deploy-production:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
    environment: production
    
    steps:
    - name: Download deployment package
      uses: actions/download-artifact@v3
      with:
        name: php-deployment-package
        
    - name: Wait for manual approval
      uses: trstringer/manual-approval@v1
      with:
        secret: ${{ github.TOKEN }}
        approvers: ${{ secrets.APPROVERS }}
        minimum-approvals: 1
        
    - name: Backup production database
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/myapp.com
          ./scripts/database.sh backup
          
    - name: Deploy to production server
      uses: appleboy/scp-action@master
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        source: "*"
        target: "/var/www/myapp.com"
        strip_components: 1
        
    - name: Run deployment script on production
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/myapp.com
          chmod +x scripts/deploy.sh
          ./scripts/deploy.sh production
          
    - name: Run smoke test on production
      uses: jawnsy/action-http-request@v1
      with:
        url: "https://myapp.com"
        method: "GET"
        
    - name: Notify deployment success
      if: success()
      uses: rtCamp/action-slack-notify@v2
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
        SLACK_CHANNEL: deployments
        SLACK_MESSAGE: " Production deployment successful! ${{ github.repository }}"
        
    - name: Rollback on failure
      if: failure()
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /var/www/myapp.com
          ./scripts/deploy.sh rollback

Step 3: Deployment Scripts

3.1 Main Deployment Script (scripts/deploy.sh)

Quote

#!/bin/bash
# scripts/deploy.sh

set -e  # Exit on any error

ENVIRONMENT=$1
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BACKUP_DIR="/var/backups/myapp"
DEPLOY_DIR="/var/www/myapp.com"

echo "🚀 Starting deployment to $ENVIRONMENT..."

# Load environment-specific configuration
if [ "$ENVIRONMENT" = "production" ]; then
    CONFIG_FILE=".env.production"
    SITE_URL="https://myapp.com"
elif [ "$ENVIRONMENT" = "staging" ]; then
    CONFIG_FILE=".env.staging"
    SITE_URL="https://staging.myapp.com"
else
    echo " Unknown environment: $ENVIRONMENT"
    exit 1
fi

# Function for rollback
rollback() {
    echo "⚠️  Rolling back deployment..."
    
    if [ -d "$DEPLOY_DIR/previous" ]; then
        rm -rf "$DEPLOY_DIR/current.broken"
        mv "$DEPLOY_DIR/current" "$DEPLOY_DIR/current.broken"
        mv "$DEPLOY_DIR/previous" "$DEPLOY_DIR/current"
        sudo systemctl restart php8.1-fpm
        sudo systemctl restart nginx
        echo " Rollback completed"
    else
        echo " No previous version to rollback to"
        exit 1
    fi
}

# Create backup directory
mkdir -p "$BACKUP_DIR"

# Step 1: Backup current version
if [ -d "$DEPLOY_DIR/current" ]; then
    echo "📦 Backing up current version..."
    cp -r "$DEPLOY_DIR/current" "$BACKUP_DIR/backup_$TIMESTAMP"
    mv "$DEPLOY_DIR/current" "$DEPLOY_DIR/previous"
fi

# Step 2: Prepare new version
echo "🛠️  Preparing new version..."
mkdir -p "$DEPLOY_DIR/current"

# Copy all files (excluding development files)
rsync -av --exclude='.git' \
          --exclude='.github' \
          --exclude='tests' \
          --exclude='*.md' \
          --exclude='composer.json' \
          --exclude='composer.lock' \
          --exclude='phpunit.xml' \
          . "$DEPLOY_DIR/current/"

# Step 3: Set up environment
echo "⚙️  Setting up environment..."
if [ -f "$CONFIG_FILE" ]; then
    cp "$CONFIG_FILE" "$DEPLOY_DIR/current/.env"
else
    echo "⚠️  Config file $CONFIG_FILE not found, using default .env"
    if [ -f ".env" ]; then
        cp ".env" "$DEPLOY_DIR/current/.env"
    fi
fi

# Step 4: Set permissions
echo "🔐 Setting permissions..."
chmod -R 755 "$DEPLOY_DIR/current"
chmod -R 775 "$DEPLOY_DIR/current/storage" 2>/dev/null || true
chmod -R 775 "$DEPLOY_DIR/current/cache" 2>/dev/null || true
chmod 644 "$DEPLOY_DIR/current/.env"

# Change ownership to web server user
chown -R www-data:www-data "$DEPLOY_DIR/current"

# Step 5: Database migrations
echo "🗄️  Running database migrations..."
cd "$DEPLOY_DIR/current"
php scripts/database.sh migrate $ENVIRONMENT

# Step 6: Composer install (if needed)
if [ -f "composer.json" ]; then
    echo "📦 Installing PHP dependencies..."
    composer install --no-dev --optimize-autoloader
fi

# Step 7: Clear caches
echo "🧹 Clearing caches..."
# Clear opcache
sudo service php8.1-fpm reload
# Clear application caches
rm -rf storage/cache/* 2>/dev/null || true
rm -rf cache/* 2>/dev/null || true

# Step 8: Update symlinks
echo "🔗 Updating symlinks..."
ln -sfn "$DEPLOY_DIR/current" "$DEPLOY_DIR/live"

# Step 9: Restart services
echo "🔄 Restarting services..."
sudo systemctl restart php8.1-fpm
sudo systemctl restart nginx

# Step 10: Run health check
echo "🏥 Running health check..."
sleep 5  # Wait for services to start
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL/health")

if [ "$HTTP_STATUS" = "200" ]; then
    echo " Deployment successful! Health check passed."
    
    # Clean up old backups (keep last 5)
    ls -dt $BACKUP_DIR/* | tail -n +6 | xargs rm -rf
    
    # Remove previous version if everything is OK
    rm -rf "$DEPLOY_DIR/previous"
else
    echo " Health check failed! HTTP Status: $HTTP_STATUS"
    rollback
    exit 1
fi

echo "🎉 Deployment to $ENVIRONMENT completed successfully!"

3.2 Database Management Script (scripts/database.sh)

 
Quote

#!/bin/bash
# scripts/database.sh

ACTION=$1
ENVIRONMENT=$2

# Database configurations
case $ENVIRONMENT in
    production)
        DB_HOST="localhost"
        DB_NAME="myapp_prod"
        DB_USER="myapp_user"
        DB_PASS_FILE="/etc/mysql/myapp_prod.pass"
        ;;
    staging)
        DB_HOST="localhost"
        DB_NAME="myapp_staging"
        DB_USER="myapp_staging_user"
        DB_PASS_FILE="/etc/mysql/myapp_staging.pass"
        ;;
    test)
        DB_HOST="localhost"
        DB_NAME="test_db"
        DB_USER="root"
        DB_PASS="root"
        ;;
    *)
        echo "Unknown environment"
        exit 1
        ;;
esac

# Read password from file if not test
if [ "$ENVIRONMENT" != "test" ]; then
    if [ -f "$DB_PASS_FILE" ]; then
        DB_PASS=$(cat "$DB_PASS_FILE")
    else
        echo "Password file not found: $DB_PASS_FILE"
        exit 1
    fi
fi

# Function to run MySQL command
mysql_cmd() {
    mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$1"
}

case $ACTION in
    migrate)
        echo "Running database migrations..."
        
        # Create migrations table if not exists
        mysql_cmd "CREATE TABLE IF NOT EXISTS migrations (
            id INT AUTO_INCREMENT PRIMARY KEY,
            migration VARCHAR(255) NOT NULL,
            batch INT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        );"
        
        # Run each migration file
        for file in database/migrations/*.sql; do
            if [ -f "$file" ]; then
                MIGRATION_NAME=$(basename "$file" .sql)
                
                # Check if migration already run
                RESULT=$(mysql_cmd "SELECT COUNT(*) FROM migrations WHERE migration = '$MIGRATION_NAME'" | tail -1)
                
                if [ "$RESULT" -eq 0 ]; then
                    echo "Running migration: $MIGRATION_NAME"
                    mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$file"
                    
                    if [ $? -eq 0 ]; then
                        mysql_cmd "INSERT INTO migrations (migration, batch) VALUES ('$MIGRATION_NAME', 1);"
                        echo " Migration completed: $MIGRATION_NAME"
                    else
                        echo " Migration failed: $MIGRATION_NAME"
                        exit 1
                    fi
                else
                    echo "⏭️  Migration already applied: $MIGRATION_NAME"
                fi
            fi
        done
        ;;
        
    backup)
        echo "Backing up database..."
        BACKUP_FILE="/var/backups/myapp/db_backup_$(date +%Y%m%d%H%M%S).sql"
        mysqldump -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" > "$BACKUP_FILE"
        gzip "$BACKUP_FILE"
        echo " Backup created: ${BACKUP_FILE}.gz"
        
        # Clean old backups (keep last 10)
        ls -t /var/backups/myapp/db_backup_*.sql.gz | tail -n +11 | xargs rm -f
        ;;
        
    seed)
        echo "Seeding database..."
        for file in database/seeds/*.sql; do
            if [ -f "$file" ]; then
                echo "Running seed: $(basename $file)"
                mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$file"
            fi
        done
        ;;
        
    status)
        echo "Database status:"
        mysql_cmd "SELECT migration, created_at FROM migrations ORDER BY id DESC LIMIT 5;"
        mysql_cmd "SELECT TABLE_NAME, TABLE_ROWS FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = '$DB_NAME';"
        ;;
        
    *)
        echo "Usage: $0 {migrate|backup|seed|status} {production|staging|test}"
        exit 1
        ;;
esac

3.3 Sample Database Migration File

 
Quote

-- database/migrations/001_create_users_table.sql
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE IF NOT EXISTS posts (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

Step 4: Server Setup Scripts

4.1 Server Provisioning Script (scripts/setup-server.sh)

Quote

#!/bin/bash
# scripts/setup-server.sh - Run this on new server

set -e

echo "🖥️  Setting up PHP/MySQL server..."

# Update system
apt-get update
apt-get upgrade -y

# Install PHP and extensions
apt-get install -y \
    php8.1 \
    php8.1-fpm \
    php8.1-mysql \
    php8.1-mbstring \
    php8.1-xml \
    php8.1-curl \
    php8.1-zip \
    php8.1-gd \
    php8.1-opcache

# Install MySQL
apt-get install -y mysql-server

# Install Nginx
apt-get install -y nginx

# Install Composer
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
chmod +x /usr/local/bin/composer

# Configure MySQL
mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_secure_password';"
mysql -e "FLUSH PRIVILEGES;"

# Create database users
mysql -e "CREATE DATABASE IF NOT EXISTS myapp_prod;"
mysql -e "CREATE DATABASE IF NOT EXISTS myapp_staging;"
mysql -e "CREATE USER IF NOT EXISTS 'myapp_user'@'localhost' IDENTIFIED BY '$(openssl rand -base64 32)';"
mysql -e "CREATE USER IF NOT EXISTS 'myapp_staging_user'@'localhost' IDENTIFIED BY '$(openssl rand -base64 32)';"
mysql -e "GRANT ALL PRIVILEGES ON myapp_prod.* TO 'myapp_user'@'localhost';"
mysql -e "GRANT ALL PRIVILEGES ON myapp_staging.* TO 'myapp_staging_user'@'localhost';"
mysql -e "FLUSH PRIVILEGES;"

# Save passwords to files
mkdir -p /etc/mysql
echo "password_here" > /etc/mysql/myapp_prod.pass
echo "password_here" > /etc/mysql/myapp_staging.pass
chmod 600 /etc/mysql/*.pass

# Configure Nginx
cat > /etc/nginx/sites-available/myapp << 'EOF'
server {
    listen 80;
    server_name myapp.com www.myapp.com;
    root /var/www/myapp.com/live/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.ht {
        deny all;
    }
    
    location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}
EOF

ln -sf /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
nginx -t
systemctl restart nginx

# Create directory structure
mkdir -p /var/www/myapp.com
mkdir -p /var/backups/myapp
chown -R www-data:www-data /var/www/myapp.com
chown -R www-data:www-data /var/backups/myapp

# Configure PHP-FPM
sed -i 's/^pm.max_children = .*/pm.max_children = 50/' /etc/php/8.1/fpm/pool.d/www.conf
sed -i 's/^pm.start_servers = .*/pm.start_servers = 5/' /etc/php/8.1/fpm/pool.d/www.conf
sed -i 's/^pm.min_spare_servers = .*/pm.min_spare_servers = 5/' /etc/php/8.1/fpm/pool.d/www.conf
sed -i 's/^pm.max_spare_servers = .*/pm.max_spare_servers = 10/' /etc/php/8.1/fpm/pool.d/www.conf

systemctl restart php8.1-fpm

# Set up firewall
ufw allow ssh
ufw allow 'Nginx Full'
ufw --force enable

echo " Server setup complete!"

4.2 Health Check Endpoint

 
Quote

<?php
// public/health.php
header('Content-Type: application/json');

$health = [
    'status' => 'healthy',
    'timestamp' => date('c'),
    'services' => []
];

// Check database
try {
    $pdo = new PDO(
        "mysql:host=" . getenv('DB_HOST') . ";dbname=" . getenv('DB_NAME'),
        getenv('DB_USER'),
        getenv('DB_PASSWORD')
    );
    $pdo->query('SELECT 1');
    $health['services']['database'] = 'healthy';
} catch (Exception $e) {
    $health['status'] = 'unhealthy';
    $health['services']['database'] = 'unhealthy: ' . $e->getMessage();
}

// Check disk space
$diskFree = disk_free_space('/');
$diskTotal = disk_total_space('/');
$diskPercent = round(($diskFree / $diskTotal) * 100, 2);
$health['services']['disk'] = [
    'free_gb' => round($diskFree / 1024 / 1024 / 1024, 2),
    'total_gb' => round($diskTotal / 1024 / 1024 / 1024, 2),
    'free_percent' => $diskPercent
];

if ($diskPercent < 10) {
    $health['status'] = 'warning';
    $health['services']['disk']['status'] = 'low_space';
}

// Check PHP version
$health['services']['php'] = [
    'version' => PHP_VERSION,
    'memory_limit' => ini_get('memory_limit')
];

http_response_code($health['status'] === 'healthy' ? 200 : 503);
echo json_encode($health, JSON_PRETTY_PRINT);

Step 5: GitHub Repository Setup

5.1 Required GitHub Secrets

Quote

STAGING_HOST: staging-server-ip
STAGING_USER: deploy-user
PRODUCTION_HOST: production-server-ip  
PRODUCTION_USER: deploy-user
SSH_PRIVATE_KEY: -----BEGIN RSA PRIVATE KEY-----\n...
SLACK_WEBHOOK: https://hooks.slack.com/services/...
APPROVERS: user1,user2,user3

5.2 .gitignore for PHP

 
Quote

# .gitignore
.env
.env.production
.env.staging
/vendor/
/node_modules/
/storage/logs/
/storage/framework/
/public/storage
composer.lock
*.log
*.cache
.DS_Store
.idea/
.vscode/
.phpunit.result.cache

Step 6: Complete Pipeline Workflow

6.1 Visual Pipeline Flow

Quote

Developer Push → GitHub → Trigger Workflow
    ↓
Run CI Pipeline:
    • Setup PHP + MySQL
    • Install Dependencies
    • Run Code Sniffer
    • Run Unit Tests
    • Integration Tests
    ↓
If Tests Pass → Build Package
    ↓
For develop branch → Auto Deploy to Staging
    ↓
For main branch → Wait for Manual Approval
    ↓
If Approved → Backup Production → Deploy → Health Check
    ↓
If Health Check Fails → Auto Rollback

6.2 Sample PHPUnit Test

 
Quote

<?php
// tests/unit/DatabaseTest.php
use PHPUnit\Framework\TestCase;

class DatabaseTest extends TestCase
{
    private $pdo;
    
    protected function setUp(): void
    {
        $host = getenv('DB_HOST') ?: 'localhost';
        $dbname = getenv('DB_NAME') ?: 'test_db';
        $username = getenv('DB_USER') ?: 'root';
        $password = getenv('DB_PASSWORD') ?: 'root';
        
        $this->pdo = new PDO(
            "mysql:host=$host;dbname=$dbname",
            $username,
            $password
        );
        $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    }
    
    public function testDatabaseConnection()
    {
        $this->assertInstanceOf(PDO::class, $this->pdo);
    }
    
    public function testCanCreateTable()
    {
        $this->pdo->exec("CREATE TABLE IF NOT EXISTS test_table (
            id INT AUTO_INCREMENT PRIMARY KEY,
            name VARCHAR(50)
        )");
        
        $tables = $this->pdo->query("SHOW TABLES LIKE 'test_table'")->fetchAll();
        $this->assertCount(1, $tables);
    }
}

Step 7: Monitoring and Maintenance

7.1 Monitoring Script (scripts/monitor.sh)

Quote

#!/bin/bash
# Cron job to monitor application

LOG_FILE="/var/log/myapp/monitor.log"
SITE_URL="https://myapp.com"

check_health() {
    RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "$SITE_URL/health" --max-time 10)
    if [ "$RESPONSE" != "200" ]; then
        echo "$(date): Health check failed! Status: $RESPONSE" >> "$LOG_FILE"
        # Send alert
        curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\" Health check failed for $SITE_URL\"}" \
            "$SLACK_WEBHOOK"
        return 1
    fi
    return 0
}

check_disk() {
    USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')
    if [ "$USAGE" -gt 90 ]; then
        echo "$(date): Disk usage above 90%: $USAGE%" >> "$LOG_FILE"
    fi
}

check_database() {
    if ! mysql -e "SELECT 1" >/dev/null 2>&1; then
        echo "$(date): Database connection failed!" >> "$LOG_FILE"
        return 1
    fi
    return 0
}

# Run checks
check_health
check_disk
check_database

echo "$(date): All checks passed" >> "$LOG_FILE"

7.2 Cron Job Setup

 
Quote

# Add to crontab (crontab -e)
*/5 * * * * /var/www/myapp.com/scripts/monitor.sh
0 2 * * * /var/www/myapp.com/scripts/database.sh backup production

📊 Complete CI/CD Dashboard Setup

8.1 Status Dashboard (HTML)

Quote

<!-- public/dashboard.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Deployment Dashboard</title>
    <style>
        body { font-family: Arial; margin: 40px; }
        .deployment { border: 1px solid #ddd; padding: 15px; margin: 10px; }
        .success { background: #d4edda; }
        .failed { background: #f8d7da; }
        .running { background: #fff3cd; }
    </style>
</head>
<body>
    <h1>🚀 CI/CD Dashboard</h1>
    
    <div id="deployments">
        <h2>Recent Deployments</h2>
        <!-- Filled by JavaScript -->
    </div>
    
    <div id="system-health">
        <h2>System Health</h2>
        <div id="health-status">Loading...</div>
    </div>
    
    <script>
        async function loadDeployments() {
            const response = await fetch('/api/deployments');
            const deployments = await response.json();
            
            let html = '';
            deployments.forEach(deploy => {
                html += `
                    <div class="deployment ${deploy.status}">
                        <h3>${deploy.environment} - ${deploy.version}</h3>
                        <p>Time: ${deploy.timestamp}</p>
                        <p>Status: ${deploy.status}</p>
                        <p>Commit: ${deploy.commit}</p>
                    </div>
                `;
            });
            document.getElementById('deployments').innerHTML += html;
        }
        
        async function checkHealth() {
            const response = await fetch('/health.php');
            const health = await response.json();
            document.getElementById('health-status').innerHTML = 
                `Status: ${health.status}<br>Database: ${health.services.database}`;
        }
        
        // Refresh every 30 seconds
        setInterval(checkHealth, 30000);
        
        loadDeployments();
        checkHealth();
    </script>
</body>
</html>

🎯 Quick Start Checklist

Pre-Deployment:

  • Server provisioned with PHP 8.1, MySQL, Nginx

  • Database created with users

  • SSH key pair generated for GitHub Actions

  • GitHub repository created

  • Secrets added to GitHub repository

  • Environment files created (.env.production, .env.staging)

First Deployment:

  • Push code to develop branch

  • Watch CI pipeline in GitHub Actions

  • Verify staging deployment

  • Test staging environment

  • Merge to main branch

  • Approve production deployment

  • Verify production deployment

  • Check health endpoint


🔧 Troubleshooting Guide

Common Issues:

  1. Database Connection Fails

Quote

# Check MySQL is running
sudo systemctl status mysql

# Check user permissions
mysql -u root -p -e "SHOW GRANTS FOR 'myapp_user'@'localhost';"

     2. PHP File Permissions

 
Quote

sudo chown -R www-data:www-data /var/www/myapp.com
sudo chmod -R 755 /var/www/myapp.com
sudo chmod -R 775 /var/www/myapp.com/storage

 

3. Nginx 502 Bad Gateway

Quote

# Check PHP-FPM
sudo systemctl status php8.1-fpm
sudo tail -f /var/log/php8.1-fpm.log

# Check socket permissions
ls -la /var/run/php/php8.1-fpm.sock

4. GitHub Actions SSH Failure

 
Quote

# Test SSH manually
ssh -i private_key deploy@server "echo test"

# Check authorized_keys
cat ~/.ssh/authorized_keys

This complete CI/CD pipeline for PHP + MySQL + HTML will:

  1. Automatically test your code

  2. Check code quality

  3. Deploy to staging automatically

  4. Require approval for production

  5. Rollback on failure

  6. Monitor application health

  7. Backup databases automatically

The pipeline is production-ready and can be customized based on your specific needs!

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

Terms of Use