نشر المشروع — إطلاق منصة ShopMetrics SaaS
الانتقال للإنتاج ليس النهاية—إنه مجرد البداية. بعد النشر، لا تزال بحاجة إلى المراقبة وإعداد التنبيهات والنسخ الاحتياطي لضمان تشغيل مستقر على مدار الساعة.
1. ما ستتعلمه
- تنسيق إنتاج Docker Compose: app/worker/scheduler/nginx/redis/mysql
- GitHub Actions CI/CD: اختبار آلي بالكامل → بناء → نشر
- فحوصات الصحة والمراقبة: تتبع أخطاء Sentry / Laravel Telescope
- استراتيجيات ترحيل قاعدة البيانات وخطط النسخ الاحتياطي
- قائمة فحص النشر وخطة التراجع
2. قصة حقيقية من يوم الإطلاق
(1) المشكلة: لم ندرك أن قاعدة البيانات لم تُرحَّل إلا بعد النشر
قاد Bob فريق ShopMetrics في أول نشر—دُفع الكود، Docker يعمل، وNginx مضبوط. لكن بمجرد أن فتحت Alice لوحة المعلومات، ظهر لها خطأ 500. تحقق Charlie من السجلات واكتشف أن جدول analytics_reports المضاف حديثاً غير موجود في قاعدة البيانات—نسوا تشغيل الترحيل. ولأنهم لم ينسخوا قاعدة البيانات احتياطياً قبل النشر، لم يكن لديهم خيار سوى تشغيل الترحيل على بيئة الإنتاج مباشرة.
(2) حل لمنهجية عملية النشر
النشر ليس مجرد "دفع الكود للإنتاج"—إنها قائمة فحص من 15 خطوة، وكل خطوة يجب أن تجتاز التحقق قبل المتابعة. CI/CD يؤتمت 10 من هذه الخطوات، تاركاً فحوصات الصحة والتحقق من الأعمال للتأكيد اليدوي فقط.
قائمة الفحص: 15 خطوة، CI/CD يؤتمت 10، يتحقق الإنسان من 5
النتيجة: 0 خطوات مفوتة، 0 حوادث بعد النشر
(3) العائد
في النشر الثاني لـ Bob، استُخدمت عملية منهجية—نشر مؤتمت بالكامل بالإضافة لفحوصات الصحة—اكتمل في 3 دقائق بدون أخطاء.
3. تنسيق إنتاج Docker Compose
(1) نظرة عامة على بنية الإنتاج
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 للإنتاج
# 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
# .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
الناتج:
# تم تنفيذ الأمر بنجاح
4. خط أنابيب CI/CD
(1) سير عمل GitHub Actions الكامل
# .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
// أنماط ترحيل آمنة للإنتاج
// ✅ آمن: إضافة عمود (غير مُعطِّل)
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');
});
الناتج:
// تم التنفيذ بنجاح
5. فحوصات الصحة والمراقبة
(1) نقاط نهاية فحص الصحة
// 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 والسياق المخصص
// 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();
});
}
}
الناتج:
// تم التنفيذ بنجاح
6. خطة نسخ قاعدة البيانات الاحتياطي
(1) استراتيجية النسخ الاحتياطي
| نوع النسخ الاحتياطي | التكرار | الاستبقاء | موقع التخزين | وقت الاستعادة |
|---|---|---|---|---|
| نسخ احتياطي كامل | يومياً الساعة 3:00 صباحاً | 30 يوماً | S3 (خارج الموقع) | 30-60 دقيقة |
| نسخ احتياطي تزايدي | كل ساعة | 7 أيام | S3 (خارج الموقع) | 60-120 دقيقة |
| Binlog | فوري | 7 أيام | محلي + S3 | PITR في أي نقطة زمنية |
(2) سكريبت نسخ احتياطي آلي
#!/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
// 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;
}
}
الناتج:
// تم التنفيذ بنجاح
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
#!/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}"
الناتج:
CONTAINER ID IMAGE STATUS
abc123 latest Up 2 hours
8. مثال شامل: العملية الكاملة لإطلاق ShopMetrics
#!/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 لـ 6 خدمات: app (x2)/queue (x5)/scheduler/mysql/redis/nginx
- GitHub Actions CI/CD: خمس مراحل—اختبار → أمان → بناء → نشر → تحقق
- نقاط نهاية فحص الصحة لـ DB/Redis/S3/Queue؛ تحقق آلي عبر CI/CD
- تتبع أخطاء Sentry + لوحة تصحيح Telescope: شبكة أمان مزدوجة
- نسخ احتياطي يومي لقاعدة البيانات إلى S3 + نسخ احتياطي binlog فوري، محفوظ لـ 30 يوماً
- قائمة فحص نشر من 15 خطوة + 4 سيناريوهات تراجع: النشر لم يعد مسألة حظ
📝 تمارين
-
تمرين أساسي (⭐): اكتب ضبط Docker Compose للإنتاج لـ ShopMetrics يتضمن ست خدمات: app، queue-worker، scheduler، mysql، redis، nginx. جميع الخدمات يجب أن تتضمن فحوصات صحة وسياسات إعادة تشغيل.
-
تمرين متقدم (⭐⭐): اكتب سير عمل GitHub Actions كامل: اختبار → تدقيق أمني → بناء Docker → نشر → اختبار دخان → إخطار Slack. أضف منطق التراجع التلقائي في حال فشل فحص الصحة.
-
تحدي (⭐⭐⭐): صمم نظام تشغيل وصيانة شامل لـ ShopMetrics—(1) نقاط نهاية فحص الصحة تغطي DB/Redis/S3/Queue/S3-backup (2) نسخ احتياطي آلي لقاعدة البيانات + سكريبتات استعادة + تمرين استعادة شهري (3) تتبع أخطاء Sentry + سياق مخصص (4) سكريبت قائمة فحص نشر آلي من 15 خطوة (5) سكريبتات تراجع (كود + قاعدة بيانات). اكتب دليل تشغيل بصيغة Markdown.



