404 Not Found

404 Not Found


nginx

نشر المشروع — إطلاق منصة ShopMetrics SaaS

الانتقال للإنتاج ليس النهاية—إنه مجرد البداية. بعد النشر، لا تزال بحاجة إلى المراقبة وإعداد التنبيهات والنسخ الاحتياطي لضمان تشغيل مستقر على مدار الساعة.

1. ما ستتعلمه


2. قصة حقيقية من يوم الإطلاق

(1) المشكلة: لم ندرك أن قاعدة البيانات لم تُرحَّل إلا بعد النشر

قاد Bob فريق ShopMetrics في أول نشر—دُفع الكود، Docker يعمل، وNginx مضبوط. لكن بمجرد أن فتحت Alice لوحة المعلومات، ظهر لها خطأ 500. تحقق Charlie من السجلات واكتشف أن جدول analytics_reports المضاف حديثاً غير موجود في قاعدة البيانات—نسوا تشغيل الترحيل. ولأنهم لم ينسخوا قاعدة البيانات احتياطياً قبل النشر، لم يكن لديهم خيار سوى تشغيل الترحيل على بيئة الإنتاج مباشرة.

(2) حل لمنهجية عملية النشر

النشر ليس مجرد "دفع الكود للإنتاج"—إنها قائمة فحص من 15 خطوة، وكل خطوة يجب أن تجتاز التحقق قبل المتابعة. CI/CD يؤتمت 10 من هذه الخطوات، تاركاً فحوصات الصحة والتحقق من الأعمال للتأكيد اليدوي فقط.

TEXT
قائمة الفحص: 15 خطوة، CI/CD يؤتمت 10، يتحقق الإنسان من 5
النتيجة: 0 خطوات مفوتة، 0 حوادث بعد النشر

(3) العائد

في النشر الثاني لـ Bob، استُخدمت عملية منهجية—نشر مؤتمت بالكامل بالإضافة لفحوصات الصحة—اكتمل في 3 دقائق بدون أخطاء.


3. تنسيق إنتاج Docker Compose

(1) نظرة عامة على بنية الإنتاج

100%
flowchart TB
    subgraph Internet["الإنترنت"]
        USER[المستخدمون]
        STRIPE[Stripe Webhooks]
    end

    subgraph LB["موازن التحميل / CDN"]
        NGINX[وكيل Nginx العكسي]
    end

    subgraph App["طبقة التطبيق"]
        APP1[حاوية التطبيق 1]
        APP2[حاوية التطبيق 2]
    end

    subgraph Worker["طبقة الخلفية"]
        Q1[عامل طابور - عالي]
        Q2[عامل طابور - افتراضي]
        SCHED[المجدول]
    end

    subgraph Data["طبقة البيانات"]
        MASTER[(MySQL الرئيسي)]
        REPLICA[(MySQL النسخة)]
        REDIS[(مجموعة Redis)]
        S3[(تخزين S3)]
    end

    subgraph Monitor["القابلية للمراقبة"]
        SENTRY[Sentry]
        TELESCOPE[Telescope]
        LOGS[تجميع السجلات]
    end

    USER --> NGINX
    STRIPE --> NGINX
    NGINX --> APP1
    NGINX --> APP2
    APP1 --> MASTER
    APP1 --> REDIS
    APP2 --> REDIS
    APP1 --> S3
    Q1 --> MASTER
    Q1 --> REDIS
    Q1 --> S3
    SCHED --> Q1
    APP1 --> REPLICA
    APP2 --> REPLICA
    APP1 --> SENTRY
    APP1 --> TELESCOPE

(2) ضبط Docker Compose للإنتاج

YAML
# docker-compose.prod.yml
services:
  app:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 512M
          cpus: '1.0'
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    healthcheck:
      test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
    networks:
      - internal

  queue-high:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan queue:work --queue=high --sleep=3 --tries=3 --max-time=3600
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 256M
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    networks:
      - internal

  queue-default:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan queue:work --queue=default --sleep=3 --tries=3 --max-time=3600
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 256M
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    networks:
      - internal

  scheduler:
    image: ghcr.io/bob/shopmetrics:latest
    restart: unless-stopped
    command: php artisan schedule:work --verbose
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - internal

  mysql:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_DATABASE: ${DB_DATABASE}
      MYSQL_USER: ${DB_USERNAME}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
      - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${DB_ROOT_PASSWORD}"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: >
      redis-server
      --requirepass ${REDIS_PASSWORD}
      --maxmemory 512mb
      --maxmemory-policy allkeys-lru
      --appendonly yes
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - internal

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./docker/nginx/production.conf:/etc/nginx/conf.d/default.conf:ro
      - ./docker/nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - internal

volumes:
  mysql_data:
  redis_data:
  storage:

networks:
  internal:
    driver: bridge

(1) ▶ مثال: ضبط ShopMetrics .env.production

BASH
# .env.production
APP_NAME=ShopMetrics
APP_ENV=production
APP_KEY=base64:xxx
APP_DEBUG=false
APP_URL=https://shopmetrics.io
APP_VERSION=1.0.0

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=shopmetrics
DB_USERNAME=shopmetrics
DB_PASSWORD=${DB_PASSWORD}
DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD}

CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
REDIS_PASSWORD=${REDIS_PASSWORD}

FILESYSTEM_DISK=s3
AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=shopmetrics-reports

STRIPE_KEY=pk_live_xxx
STRIPE_SECRET=sk_live_xxx
STRIPE_WEBHOOK_SECRET=whsec_xxx

SENTRY_LARAVEL_DSN=https://xxx@sentry.io/xxx
SENTRY_TRACES_SAMPLE_RATE=0.2

LOG_CHANNEL=daily
LOG_LEVEL=warning

الناتج:

TEXT
# تم تنفيذ الأمر بنجاح

4. خط أنابيب CI/CD

(1) سير عمل GitHub Actions الكامل

YAML
# .github/workflows/deploy-production.yml
name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:  # خيار التشغيل اليدوي

concurrency: production  # نشر واحد فقط في كل مرة

env:
  REGISTRY: ghcr.io
  IMAGE: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_DATABASE: shopmetrics_test
          MYSQL_USER: test
          MYSQL_PASSWORD: test
          MYSQL_ROOT_PASSWORD: test
        ports: ['3306:3306']
      redis:
        image: redis:7-alpine
        ports: ['6379:6379']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: xdebug
      - name: Install & Test
        run: |
          composer install --no-progress
          cp .env.example .env
          php artisan key:generate
          php artisan test --parallel --coverage-text=coverage.txt
      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage.txt

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: composer install --no-dev
      - name: Security audit
        run: composer audit

  build:
    needs: [test, security]
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /opt/shopmetrics
            export IMAGE_TAG=${{ github.sha }}

            # سحب أحدث صورة
            docker compose -f docker-compose.prod.yml pull app queue-high queue-default scheduler

            # تحديث متدرج: بدء الحاويات الجديدة
            docker compose -f docker-compose.prod.yml up -d --no-deps --scale app=3 app
            sleep 10

            # فحص صحة الحاويات الجديدة
            for i in $(seq 1 5); do
              if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
                echo "✓ فحص الصحة ناجح"
                break
              fi
              echo "في انتظار فحص الصحة... ($i/5)"
              sleep 5
            done

            # تشغيل الترحيلات
            docker compose -f docker-compose.prod.yml exec -T app php artisan migrate --force

            # إعادة تشغيل العمال (بشكل سلس)
            docker compose -f docker-compose.prod.yml exec -T app php artisan queue:restart

            # تقليص الحاويات القديمة
            docker compose -f docker-compose.prod.yml up -d --scale app=2

            # مسح وإعادة بناء التخزين المؤقت
            docker compose -f docker-compose.prod.yml exec -T app php artisan optimize:clear
            docker compose -f docker-compose.prod.yml exec -T app php artisan optimize

            echo "✓ اكتمل النشر: $IMAGE_TAG"

  verify:
    needs: deploy
    runs-on: ubuntu-latest
    steps:
      - name: Smoke test production
        run: |
          STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://shopmetrics.io/health)
          if [ "$STATUS" != "200" ]; then
            echo "✗ فشل فحص الصحة: HTTP $STATUS"
            exit 1
          fi
          echo "✓ فحص صحة الإنتاج ناجح"

      - name: Notify Slack on success
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text":"✅ تم نشر ShopMetrics بنجاح: ${{ github.sha }}"}

      - name: Notify Slack on failure
        if: failure()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {"text":"🚨 فشل نشر ShopMetrics: ${{ github.sha }}"}

(1) ▶ مثال: استراتيجية ترحيل قاعدة بيانات ShopMetrics

PHP
// أنماط ترحيل آمنة للإنتاج

// ✅ آمن: إضافة عمود (غير مُعطِّل)
Schema::table('orders', function (Blueprint $table) {
    $table->string('shipping_method')->nullable()->after('status');
    $table->index('shipping_method');
});

// ✅ آمن: إضافة فهرس (متزامن في PostgreSQL)
// MySQL: استخدم ALGORITHM=INPLACE لإنشاء فهرس غير محظر
DB::statement('ALTER TABLE orders ADD INDEX idx_orders_shipping (shipping_method) ALGORITHM=INPLACE');

// ⚠️ حذر: إزالة عمود (ترحيل من خطوتين)
// الخطوة 1: وضع علامة مهمل (انشر أولاً، أزل مراجع الكود)
Schema::table('orders', function (Blueprint $table) {
    // أبقِ العمود القديم، أضف عموداً جديداً
    $table->renameColumn('shipping_method', 'deprecated_shipping_method');
});

// الخطوة 2: أزل بعد أن لم يعد الكود يشير إليه (النشر التالي)
Schema::table('orders', function (Blueprint $table) {
    $table->dropColumn('deprecated_shipping_method');
});

// ❌ خطير: تغيير نوع العمود (يقفل الجدول)
// استخدم خطوتين: أضف عمود جديد → رحّل البيانات → أزل العمود القديم
Schema::table('orders', function (Blueprint $table) {
    $table->unsignedBigInteger('total_cents_new')->nullable()->after('total_cents');
});

// ترحيل البيانات في ملف ترحيل منفصل
DB::statement('UPDATE orders SET total_cents_new = total_cents WHERE total_cents_new IS NULL');

Schema::table('orders', function (Blueprint $table) {
    $table->dropColumn('total_cents');
    $table->renameColumn('total_cents_new', 'total_cents');
});

الناتج:

TEXT
// تم التنفيذ بنجاح

5. فحوصات الصحة والمراقبة

(1) نقاط نهاية فحص الصحة

PHP
// routes/web.php
Route::get('/health', function () {
    $start = microtime(true);
    $checks = [];

    // فحص قاعدة البيانات
    try {
        DB::connection()->getPdo();
        $checks['database'] = 'ok';
    } catch (\Throwable $e) {
        $checks['database'] = 'fail: ' . $e->getMessage();
    }

    // فحص Redis
    try {
        Cache::put('health_check', 'ok', 10);
        $checks['redis'] = Cache::get('health_check') === 'ok' ? 'ok' : 'fail';
    } catch (\Throwable $e) {
        $checks['redis'] = 'fail: ' . $e->getMessage();
    }

    // فحص S3
    try {
        Storage::disk('s3')->put('health_check.txt', 'ok');
        $checks['storage'] = Storage::disk('s3')->get('health_check.txt') === 'ok' ? 'ok' : 'fail';
    } catch (\Throwable $e) {
        $checks['storage'] = 'fail: ' . $e->getMessage();
    }

    // فحص الطابور
    try {
        $size = Queue::size('default');
        $checks['queue'] = $size < 10000 ? "ok (size: {$size})" : "warn (size: {$size})";
    } catch (\Throwable $e) {
        $checks['queue'] = 'fail: ' . $e->getMessage();
    }

    $latency = round((microtime(true) - $start) * 1000, 2);
    $allOk = collect($checks)->every(fn ($v) => str_starts_with($v, 'ok'));

    return response()->json([
        'status' => $allOk ? 'healthy' : 'unhealthy',
        'checks' => $checks,
        'latency_ms' => $latency,
        'version' => config('app.version'),
        'timestamp' => now()->toIso8601String(),
    ], $allOk ? 200 : 503);
});

(2) مقارنة أدوات المراقبة

الأداة النوع المستوى المجاني حالات الاستخدام
Sentry تتبع الأخطاء 5 آلاف حدث/شهر التقاط الاستثناءات + تتبع الأداء
Laravel Telescope لوحة تصحيح مجاني التطوير/التصحيح (وصول مقيد في الإنتاج)
Prometheus + Grafana مراقبة المقاييس استضافة ذاتية مجانية مقاييس النظام + لوحات معلومات مخصصة
UptimeRobot مراقبة التوفر 50 مراقب تنبيهات فحص صحة HTTP
Algolia/Meilisearch بحث السجلات مجاني محدود تجميع السجلات والبحث

(1) ▶ مثال: ضبط Sentry لـ ShopMetrics والسياق المخصص

PHP
// config/sentry.php
return [
    'dsn' => env('SENTRY_LARAVEL_DSN'),
    'traces_sample_rate' => env('SENTRY_TRACES_SAMPLE_RATE', 0.2),
    'send_default_pii' => false,
    'environment' => app()->environment(),
    'release' => config('app.version'),
];

// app/Exceptions/Handler.php
class Handler extends ExceptionHandler
{
    public function report(Throwable $e): void
    {
        if (app()->bound('sentry') && $this->shouldReport($e)) {
            \Sentry\configureScope(function (\Sentry\State\Scope $scope) {
                if ($user = auth()->user()) {
                    $scope->setUser([
                        'id' => $user->id,
                        'email' => $user->email,
                        'tenant_id' => $user->tenant_id,
                    ]);
                }

                $scope->setTag('app_version', config('app.version'));
                $scope->setExtra('request_url', request()->url());
                $scope->setExtra('request_method', request()->method());
            });
        }

        parent::report($e);
    }
}

// app/Providers/AppServiceProvider.php
class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        if (app()->environment('local')) {
            Telescope::listenStorageNotifications();
        }

        Telescope::filter(function (IncomingEntry $entry) {
            if (app()->environment('local')) return true;
            return $entry->isReportableException() ||
                   $entry->isFailedJob() ||
                   $entry->isScheduledTask() ||
                   $entry->hasMonitoredTag();
        });
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

6. خطة نسخ قاعدة البيانات الاحتياطي

(1) استراتيجية النسخ الاحتياطي

نوع النسخ الاحتياطي التكرار الاستبقاء موقع التخزين وقت الاستعادة
نسخ احتياطي كامل يومياً الساعة 3:00 صباحاً 30 يوماً S3 (خارج الموقع) 30-60 دقيقة
نسخ احتياطي تزايدي كل ساعة 7 أيام S3 (خارج الموقع) 60-120 دقيقة
Binlog فوري 7 أيام محلي + S3 PITR في أي نقطة زمنية

(2) سكريبت نسخ احتياطي آلي

BASH
#!/bin/bash
# backup-mysql.sh — نسخ احتياطي يومي لـ MySQL إلى S3
set -e

DB_NAME="shopmetrics"
S3_BUCKET="s3://shopmetrics-backups/mysql"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="/tmp/shopmetrics_${DATE}.sql.gz"

echo "→ بدء النسخ الاحتياطي: ${DATE}"

# تفريغ بلقطة متسقة (بدون قفل)
mysqldump \
  --host=mysql \
  --user=${DB_USERNAME} \
  --password=${DB_PASSWORD} \
  --single-transaction \
  --routines \
  --triggers \
  --quick \
  ${DB_NAME} | gzip > ${BACKUP_FILE}

# رفع إلى S3
aws s3 cp ${BACKUP_FILE} ${S3_BUCKET}/daily/shopmetrics_${DATE}.sql.gz

# تنظيف الملف المحلي
rm -f ${BACKUP_FILE}

# تنظيف النسخ الاحتياطية الأقدم من 30 يوماً
aws s3 rm ${S3_BUCKET}/daily/ --recursive --exclude "*" --include "shopmetrics_*" \
  --query "Contents[?LastModified<='$(date -d '-30 days' +%Y-%m-%d)'].Key"

echo "✓ اكتمل النسخ الاحتياطي: shopmetrics_${DATE}.sql.gz"

# التحقق من سلامة النسخة الاحتياطية
LATEST=$(aws s3 ls ${S3_BUCKET}/daily/ | tail -1 | awk '{print $4}')
aws s3 cp ${S3_BUCKET}/daily/${LATEST} /tmp/verify.sql.gz
if gzip -t /tmp/verify.sql.gz; then
    echo "✓ تم التحقق من سلامة النسخة الاحتياطية"
else
    echo "✗ فشل التحقق من سلامة النسخة الاحتياطية"
    exit 1
fi
rm -f /tmp/verify.sql.gz

(1) ▶ مثال: جدول نسخ احتياطي آلي لـ ShopMetrics

PHP
// routes/console.php
use Illuminate\Support\Facades\Schedule;

// نسخ احتياطي يومي لقاعدة البيانات
Schedule::command('shopmetrics:backup-database')
    ->dailyAt('03:00')
    ->onOneServer()
    ->withoutOverlapping()
    ->emailOutputOnFailure('ops@shopmetrics.io');

// مزامنة تخزين S3 كل ساعة (لملفات التقارير)
Schedule::command('shopmetrics:sync-storage-backup')
    ->hourly()
    ->onOneServer();

// اختبار سلامة النسخ الاحتياطي أسبوعياً
Schedule::command('shopmetrics:verify-backup')
    ->weeklyOn(Schedule::SUNDAY, '04:00')
    ->onOneServer()
    ->emailOutputOnFailure('ops@shopmetrics.io');

// app/Console/Commands/BackupDatabase.php
class BackupDatabase extends Command
{
    protected $signature = 'shopmetrics:backup-database';
    protected $description = 'نسخ احتياطي لقاعدة بيانات MySQL إلى S3';

    public function handle(): int
    {
        $filename = 'shopmetrics_' . now()->format('Ymd_His') . '.sql.gz';
        $tempPath = storage_path('app/backups/' . $filename);

        $this->info('بدء النسخ الاحتياطي لقاعدة البيانات...');

        $command = sprintf(
            'mysqldump --host=%s --user=%s --password=%s --single-transaction %s | gzip > %s',
            config('database.connections.mysql.host'),
            config('database.connections.mysql.username'),
            config('database.connections.mysql.password'),
            config('database.connections.mysql.database'),
            $tempPath
        );

        $exitCode = Process::run($command)->exitCode();

        if ($exitCode !== 0) {
            $this->error('فشل النسخ الاحتياطي!');
            return self::FAILURE;
        }

        $s3Path = 'mysql/daily/' . $filename;
        Storage::disk('s3-backup')->put($s3Path, file_get_contents($tempPath));
        unlink($tempPath);

        $this->info("تم رفع النسخة الاحتياطية: {$s3Path}");
        return self::SUCCESS;
    }
}

الناتج:

TEXT
// تم التنفيذ بنجاح

7. قائمة فحص النشر وخطة التراجع

(1) قائمة فحص الإطلاق

# عنصر الفحص الأمر/الإجراء الأتمتة المعيار
1 الكود اجتاز الاختبار php artisan test CI 0 إخفاقات
2 التدقيق الأمني اجتاز composer audit CI 0 ثغرات
3 بناء صورة Docker docker build CI بناء ناجح
4 سحب أحدث صورة docker compose pull CD الصورة موجودة
5 بدء حاوية جديدة docker compose up -d CD الحاوية تعمل
6 فحص الصحة ناجح curl /health CD HTTP 200
7 ترحيل قاعدة البيانات migrate --force CD 0 أخطاء
8 تسخين التخزين المؤقت config:cache + route:cache CD الأمر ناجح
9 إعادة تشغيل عامل الطابور queue:restart CD العامل أُعيد تشغيله
10 تنظيف الحاويات القديمة docker image prune CD مساحة القرص طبيعية
11 اختبار دخان الأعمال اختبار يدوي للعمليات الرئيسية يدوي الوظائف طبيعية
12 تأكيد المراقبة لا شذوذ في Sentry/Telescope يدوي 0 أخطاء في 5 دقائق
13 فحص السجلات لا ERROR في السجلات يدوي لا استثناءات جديدة
14 التحقق من الأداء تحميل لوحة المعلومات < 500 مللي ثانية يدوي p95 < 500 مللي ثانية
15 إخطار الفريق إخطار Slack/بريد إلكتروني آلي تم الإرسال

(2) خطة التراجع

السيناريو طريقة الكشف إجراء التراجع وقت الاستعادة
فشل فحص الصحة كشف CI/CD آلي docker compose pull <previous-tag> + up أقل من دقيقة
فشل الترحيل سجلات CI/CD migrate:rollback + تراجع الكود أقل من 5 دقائق
خطأ منطق الأعمال ملاحظات المستخدمين/المراقبة تراجع أزرق-أخضر للفتحة السابقة أقل من 30 ثانية
تلف قاعدة البيانات تنبيهات المراقبة استعادة من نسخة S3 + إعادة Binlog 30-120 دقيقة

(1) ▶ مثال: سكريبت تراجع ShopMetrics

BASH
#!/bin/bash
# rollback.sh — سكريبت تراجع طوارئ
set -e

COMPOSE_FILE="docker-compose.prod.yml"
BACKUP_DIR="/opt/shopmetrics/backups"

echo "=== تراجع طوارئ ==="

# الحصول على وسم الصورة السابقة
PREVIOUS_TAG=$(cat ${BACKUP_DIR}/last_successful_deploy_tag.txt)
CURRENT_TAG=$(cat ${BACKUP_DIR}/current_deploy_tag.txt)

echo "الحالي: ${CURRENT_TAG}"
echo "التراجع إلى: ${PREVIOUS_TAG}"

# الخطوة 1: تعيين وسم الصورة للإصدار السابق
export IMAGE_TAG=${PREVIOUS_TAG}

# الخطوة 2: سحب الصورة السابقة
docker compose -f ${COMPOSE_FILE} pull app queue-high queue-default scheduler

# الخطوة 3: بدء حاويات الإصدار السابق
docker compose -f ${COMPOSE_FILE} up -d --no-deps --scale app=2 app
docker compose -f ${COMPOSE_FILE} up -d --no-deps queue-high queue-default scheduler

# الخطوة 4: الانتظار والتحقق
sleep 10
if curl -sf http://localhost:8080/health > /dev/null; then
    echo "✓ فحص الصحة ناجح بعد التراجع"
else
    echo "✗ فحص الصحة فشل بعد التراجع — يتطلب تدخلاً يدوياً"
    exit 1
fi

# الخطوة 5: التحقق من الحاجة لتراجع ترحيل قاعدة البيانات
echo "⚠ تحقق من الحاجة لتراجع ترحيلات قاعدة البيانات:"
echo "  شغّل: docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate:status"
echo "  إذا لزم: docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate:rollback --step=N"

# الخطوة 6: تحديث الوسم الحالي
echo "${PREVIOUS_TAG}" > ${BACKUP_DIR}/current_deploy_tag.txt

# الخطوة 7: إخطار الفريق
echo "⚠ اكتمل التراجع — أبلغ الفريق فوراً"
echo "  تم التراجع من ${CURRENT_TAG} إلى ${PREVIOUS_TAG}"

الناتج:

TEXT
CONTAINER ID   IMAGE     STATUS    
abc123         latest    Up 2 hours

8. مثال شامل: العملية الكاملة لإطلاق ShopMetrics

BASH
#!/bin/bash
# ============================================
# شامل: سكريبت نشر ShopMetrics الكامل
# يغطي: نسخ احتياطي → نشر → ترحيل → تحقق → خطة تراجع
# ============================================

set -euo pipefail

DEPLOY_TAG=${1:-$(git rev-parse --short HEAD)}
COMPOSE_FILE="docker-compose.prod.yml"
BACKUP_DIR="/opt/shopmetrics/backups"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"

log() { echo "[$(date +%H:%M:%S)] $1"; }
notify() { curl -s -X POST "${SLACK_WEBHOOK}" -H 'Content-type: application/json' --data "{\"text\":\"$1\"}" > /dev/null 2>&1 || true; }

# قبل النشر: نسخ احتياطي
log "→ الخطوة 1/10: نسخ احتياطي لقاعدة البيانات"
php artisan shopmetrics:backup-database
log "✓ اكتمل النسخ الاحتياطي"

# قبل النشر: حفظ الحالة الحالية
log "→ الخطوة 2/10: حفظ الحالة الحالية"
cat ${BACKUP_DIR}/current_deploy_tag.txt > ${BACKUP_DIR}/last_successful_deploy_tag.txt 2>/dev/null || true
echo "${DEPLOY_TAG}" > ${BACKUP_DIR}/current_deploy_tag.txt
log "✓ تم حفظ الحالة (التراجع إلى: $(cat ${BACKUP_DIR}/last_successful_deploy_tag.txt 2>/dev/null || echo 'غير متوفر'))"

# النشر: السحب والبدء
log "→ الخطوة 3/10: سحب الصورة ${DEPLOY_TAG}"
export IMAGE_TAG=${DEPLOY_TAG}
docker compose -f ${COMPOSE_FILE} pull app queue-high queue-default scheduler

log "→ الخطوة 4/10: بدء الحاويات الجديدة"
docker compose -f ${COMPOSE_FILE} up -d --no-deps --remove-orphans app queue-high queue-default scheduler

# فحص الصحة مع إعادة المحاولة
log "→ الخطوة 5/10: فحص الصحة"
for i in $(seq 1 6); do
    if curl -sf http://localhost:8080/health > /dev/null 2>&1; then
        log "✓ فحص الصحة ناجح"
        break
    fi
    if [ $i -eq 6 ]; then
        log "✗ فشل فحص الصحة بعد 30 ثانية — جارٍ التراجع"
        bash /opt/shopmetrics/rollback.sh
        notify "🚨 فشل نشر ShopMetrics (فحص الصحة) — تم التراجع"
        exit 1
    fi
    log "  في الانتظار... ($i/6)"
    sleep 5
done

# الترحيل
log "→ الخطوة 6/10: تشغيل الترحيلات"
docker compose -f ${COMPOSE_FILE} exec -T app php artisan migrate --force
if [ $? -ne 0 ]; then
    log "✗ فشل الترحيل — جارٍ التراجع"
    bash /opt/shopmetrics/rollback.sh
    notify "🚨 فشل نشر ShopMetrics (الترحيل) — تم التراجع"
    exit 1
fi
log "✓ اكتملت الترحيلات"

# التحسين
log "→ الخطوة 7/10: التخزين المؤقت"
docker compose -f ${COMPOSE_FILE} exec -T app php artisan optimize:clear
docker compose -f ${COMPOSE_FILE} exec -T app php artisan optimize
docker compose -f ${COMPOSE_FILE} exec -T app php artisan queue:restart
log "✓ تم إعادة بناء التخزين المؤقت"

# التحقق بعد النشر
log "→ الخطوة 8/10: اختبارات الدخان"
SMOKE_PASS=true
for endpoint in "/health" "/api/v1/plans"; do
    STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://shopmetrics.io${endpoint}")
    if [ "$STATUS" -ge 400 ]; then
        log "✗ فشل اختبار الدخان: ${endpoint} → HTTP ${STATUS}"
        SMOKE_PASS=false
    fi
done
if [ "$SMOKE_PASS" = false ]; then
    log "✗ فشلت اختبارات الدخان — جارٍ التراجع"
    bash /opt/shopmetrics/rollback.sh
    notify "🚨 فشل نشر ShopMetrics (اختبار الدخان) — تم التراجع"
    exit 1
fi
log "✓ نجحت اختبارات الدخان"

# التنظيف
log "→ الخطوة 9/10: التنظيف"
docker image prune -f > /dev/null
log "✓ تم تنظيف الصور القديمة"

# النجاح
log "→ الخطوة 10/10: التحقق النهائي"
sleep 5
ERRORS=$(docker compose -f ${COMPOSE_FILE} logs --since 2m app 2>&1 | grep -c "ERROR" || true)
if [ "${ERRORS}" -gt 5 ]; then
    log "⚠ معدل أخطاء مرتفع مكتشف: ${ERRORS} أخطاء في دقيقتين"
    notify "⚠️ تم نشر ShopMetrics لكن معدل الأخطاء مرتفع: ${ERRORS} أخطاء"
else
    log "✓ نشر ناجح: ${DEPLOY_TAG}"
    notify "✅ تم نشر ShopMetrics بنجاح: ${DEPLOY_TAG}"
fi

log "=== اكتمل النشر ==="

❓ أسئلة شائعة

س هل يجب استخدام Docker Compose أم Kubernetes في بيئة الإنتاج؟
ج Docker Compose كافٍ حتى 5 خوادم، لأنه يبسط العمليات. انتقل إلى Kubernetes عند تجاوز 5 خوادم أو الحاجة لتوسع تلقائي. بدأت ShopMetrics بـ Docker Compose وستنظر في Kubernetes عند تجاوز عدد المستخدمين 10,000.
س ماذا أفعل إذا فشل الترحيل في بيئة الإنتاج؟
ج في CI/CD، الترحيل الفاشل يحظر النشر تلقائياً ولا يحول حركة المرور. فقط تراجع إلى إصدار الكود السابق (مخطط قاعدة البيانات يبقى متوافقاً مع الإصدار القديم، لأن الترحيلات تتبع مبدأ "أضف أولاً، ثم احذف").
س هل المستوى المجاني من Sentry كافٍ؟
ج 5,000 حدث شهرياً كافٍ للمراحل المبكرة. عند تجاوز هذا الحد، يمكن التبديل لخطة الدفع حسب الاستخدام أو استضافة Sentry ذاتياً (مفتوح المصدر ومجاني، لكنك ستحتاج لصيانة خادمك الخاص). نوصي بإبقاء Sentry مفعلاً دائماً في بيئات الإنتاج.
س كم يستغرق نسخ قاعدة البيانات احتياطياً واستعادتها؟
ج استعادة كاملة لقاعدة بيانات بحجم 10 غيغابايت تستغرق حوالي 30 دقيقة (شاملة التنزيل من S3 والاستيراد). تفعيل binlog يسمح بالاستعادة اللحظية (PITR)، مع وقت استعادة حوالي 60-120 دقيقة. نفذ تمريناً على الاستعادة مرة واحدة على الأقل شهرياً.
س كيف أعرف متى يجب التوسع؟
ج راقب المقاييس التالية: CPU > 70% لمدة 5 دقائق متتالية، تراكم الطابور > 1,000، وزمن استجابة p95 > 500 مللي ثانية. اضبط عتبات التنبيه؛ عند تفعيلها، تحقق أولاً مما إذا كان التحسين ممكناً. توسع فقط بعد تأكيد أن المشكلة تتعلق بالسعة.
س ماذا يحدث لقاعدة البيانات بعد التراجع؟
ج اتبع مبدأ ترحيل "أضف أولاً، ثم احذف"—الإصدار الجديد يضيف أعمدة فقط بدون تعديل الموجود منها، لذلك يبقى هيكل قاعدة البيانات متوافقاً بعد تراجع الكود. عمليات الحذف تؤجل إصداراً واحداً على الأقل. بهذه الطريقة، التراجع يؤثر على الكود فقط، وليس قاعدة البيانات.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): اكتب ضبط Docker Compose للإنتاج لـ ShopMetrics يتضمن ست خدمات: app، queue-worker، scheduler، mysql، redis، nginx. جميع الخدمات يجب أن تتضمن فحوصات صحة وسياسات إعادة تشغيل.

  2. تمرين متقدم (⭐⭐): اكتب سير عمل GitHub Actions كامل: اختبار → تدقيق أمني → بناء Docker → نشر → اختبار دخان → إخطار Slack. أضف منطق التراجع التلقائي في حال فشل فحص الصحة.

  3. تحدي (⭐⭐⭐): صمم نظام تشغيل وصيانة شامل لـ ShopMetrics—(1) نقاط نهاية فحص الصحة تغطي DB/Redis/S3/Queue/S3-backup (2) نسخ احتياطي آلي لقاعدة البيانات + سكريبتات استعادة + تمرين استعادة شهري (3) تتبع أخطاء Sentry + سياق مخصص (4) سكريبت قائمة فحص نشر آلي من 15 خطوة (5) سكريبتات تراجع (كود + قاعدة بيانات). اكتب دليل تشغيل بصيغة Markdown.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%