404 Not Found

404 Not Found


nginx

دليل عملي لنشر Laravel في الإنتاج

النشر هو الخطوة الأخيرة لإطلاق المنتج للعالم — خطأ واحد قد يؤدي إلى رؤية جميع المستخدمين لصفحة خطأ 500.

1. ما ستتعلمه


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

(1) المشكلة: النشر اليدوي يواجه دائمًا مشاكل

في كل مرة ينشر Bob فيها ShopMetrics، يجب عليه: الدخول عبر SSH إلى الخادم → git pull → composer install → php artisan migrate → php artisan config:cache → إعادة تشغيل عامل الطابور → إعادة تشغيل PHP-FPM. مع خمسة خوادم، تستغرق هذه العملية اليدوية 40 دقيقة. الشهر الماضي، نسي تشغيل الترحيل مرة واحدة، مما تسبب في خطأ 500 على الموقع بالكامل بخسارة 2,000 دولار. قال Charlie: "أنت تنشر منتج SaaS لعام 2024 بطريقة التسعينيات."

(2) حل Docker + CI/CD

حاويات Docker تضمن أن كل بيئة متسقة تمامًا، وخطوط CI/CD تقلل النشر إلى أمر "git push" واحد — الاختبار والبناء والنشر جميعها مؤتمتة بالكامل.

TEXT
git push → GitHub Actions → اختبار → بناء Docker → نشر → فحص صحي → تم

(3) النتيجة

بعد أن نفذ Bob نظام CI/CD، انخفض وقت النشر من 40 دقيقة إلى 3 دقائق، وانخفض معدل الخطأ من 10% إلى 0%, ولم يعد يحتاج للدخول عبر SSH إلى الخادم في منتصف الليل.


3. حاويات Docker

(1) Dockerfile متعدد المراحل

DOCKERFILE
# Dockerfile
# المرحلة 1: بناء التبعيات
FROM composer:2.7 AS build
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-progress
COPY . .
RUN php artisan route:cache && \
    php artisan config:cache && \
    php artisan view:cache

# المرحلة 2: صورة الإنتاج
FROM php:8.3-fpm-alpine AS production
WORKDIR /var/www/html

RUN apk add --no-cache \
    nginx \
    supervisor \
    pdo_mysql \
    gd \
    zip \
    redis

RUN docker-php-ext-install pdo_mysql gd zip opcache redis

COPY --from=build /app /var/www/html
COPY docker/opcache.ini /usr/local/etc/php/conf.d/opcache.ini
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf

RUN chown -R www-data:www-data /var/www/html/storage /var/www/html/bootstrap/cache

EXPOSE 8080
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

(2) تنسيق Docker Compose

YAML
# docker-compose.prod.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    ports:
      - "8080:8080"
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 30s
      timeout: 5s
      retries: 3

  queue:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    command: php artisan queue:work --queue=high,default --sleep=3 --tries=3
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
    volumes:
      - storage:/var/www/html/storage

  scheduler:
    build:
      context: .
      dockerfile: Dockerfile
    restart: unless-stopped
    command: php artisan schedule:work
    env_file:
      - .env.production
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy

  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_PASSWORD}
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    command: redis-server --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  mysql_data:
  redis_data:
  storage:

(1) ▶ مثال: بيئة تطوير Docker في ShopMetrics

YAML
# docker-compose.yml (التطوير)
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - .:/var/www/html
    environment:
      DB_HOST: mysql
      REDIS_HOST: redis
    depends_on:
      - mysql
      - redis
      - mailpit

  mysql:
    image: mysql:8.0
    ports:
      - "3306:3306"
    environment:
      MYSQL_DATABASE: shopmetrics
      MYSQL_USER: shopmetrics
      MYSQL_PASSWORD: secret
      MYSQL_ROOT_PASSWORD: secret
    volumes:
      - mysql_dev:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  mailpit:
    image: axllent/mailpit
    ports:
      - "1025:1025"   # SMTP
      - "8025:8025"   # واجهة الويب

volumes:
  mysql_dev:

الناتج:

TEXT
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

4. تهيئة Nginx + PHP-FPM

(1) تهيئة Nginx

NGINX
# docker/nginx.conf
server {
    listen 8080;
    server_name shopmetrics.io api.shopmetrics.io;
    root /var/www/html/public;
    index index.php;

    charset utf-8;

    # رؤوس الأمان
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";
    add_header Referrer-Policy "strict-origin-when-cross-origin";

    # الحد الأقصى لحجم الرفع
    client_max_body_size 20M;

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    location ~ \.php$ {
        fastcgi_pass 127.0.0.1:9000;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        fastcgi_hide_header X-Powered-By;

        # مخازن مؤقتة للأحمال الكبيرة
        fastcgi_buffer_size 128k;
        fastcgi_buffers 4 256k;
        fastcgi_busy_buffers_size 256k;

        # المهلات
        fastcgi_connect_timeout 60s;
        fastcgi_send_timeout 60s;
        fastcgi_read_timeout 60s;
    }

    # رفض الوصول للمحتوى المخفي
    location ~ /\.(?!well-known).* {
        deny all;
    }
}

(2) تهيئة SSL (Let's Encrypt)

NGINX
# إنهاء SSL عند الوكيل العكسي أو موازن الحمل
server {
    listen 443 ssl http2;
    server_name shopmetrics.io;

    ssl_certificate /etc/letsencrypt/live/shopmetrics.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shopmetrics.io/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    location / {
        proxy_pass http://app:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# إعادة توجيه HTTP إلى HTTPS
server {
    listen 80;
    server_name shopmetrics.io;
    return 301 https://$server_name$request_uri;
}

(1) ▶ مثال: توجيه النطاقات الفرعية متعددة المستأجرين بـ Nginx في ShopMetrics

NGINX
# توجيه النطاقات الفرعية للمستأجرين
server {
    listen 443 ssl http2;
    server_name ~^(?<tenant>[^\.]+)\.shopmetrics\.io$;

    ssl_certificate /etc/letsencrypt/live/shopmetrics.io/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/shopmetrics.io/privkey.pem;

    # تمرير الاسم المختصر للمستأجر إلى Laravel
    location / {
        proxy_pass http://app:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Tenant $tenant;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

الناتج:

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

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

(1) سير عمل GitHub Actions

YAML
# .github/workflows/deploy.yml
name: Deploy ShopMetrics

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ 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 dependencies
        run: composer install --no-progress --prefer-dist
      - name: Copy .env
        run: cp .env.example .env && php artisan key:generate
      - name: Run tests
        run: php artisan test --parallel --coverage-text
      - name: Check code style
        run: vendor/bin/pint --test

  build:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to production
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            cd /var/www/shopmetrics
            docker compose -f docker-compose.prod.yml pull
            docker compose -f docker-compose.prod.yml up -d --remove-orphans
            docker compose -f docker-compose.prod.yml exec app php artisan migrate --force
            docker compose -f docker-compose.prod.yml exec app php artisan config:cache
            docker compose -f docker-compose.prod.yml exec app php artisan queue:restart
            sleep 5
            curl -sf http://localhost:8080/health || exit 1
            echo "Deploy successful!"

(2) مخطط تدفق النشر

100%
flowchart LR
    A[git push main] --> B[GitHub Actions]
    B --> C[تشغيل الاختبارات]
    C -->|نجح| D[بناء صورة Docker]
    C -->|فشل| E[إشعار + حظر]
    D --> F[دفع إلى السجل]
    F --> G[SSH إلى الإنتاج]
    G --> H[docker compose pull]
    H --> I[docker compose up -d]
    I --> J[php artisan migrate]
    J --> K[تخزين مؤقت + إعادة تشغيل الطابور]
    K --> L[فحص صحي]
    L -->|موافق| M[اكتمل النشر]
    L -->|فشل| N[تراجع تلقائي]

(1) ▶ مثال: نقطة نهاية فحص صحي لنشر ShopMetrics

PHP
// routes/web.php
Route::get('/health', function () {
    $checks = [
        'database' => fn () => DB::connection()->getPdo() ? 'ok' : 'fail',
        'redis' => fn () => Cache::put('health_check', 'ok', 10) ? 'ok' : 'fail',
        'storage' => fn () => Storage::put('health_check', 'ok') ? 'ok' : 'fail',
    ];

    $results = collect($checks)->map(fn ($check) => $check());

    if ($results->contains('fail')) {
        return response()->json([
            'status' => 'unhealthy',
            'checks' => $results,
        ], 503);
    }

    return response()->json([
        'status' => 'healthy',
        'checks' => $results,
        'timestamp' => now()->toIso8601String(),
        'version' => config('app.version', 'unknown'),
    ]);
});

الناتج:

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

6. قائمة فحص النشر

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

الخطوة الأمر/الإجراء الوصف
1 فحص تهيئة .env APP_ENV=production, APP_DEBUG=false
2 php artisan key:generate التحقق من تعيين APP_KEY
3 php artisan migrate --force تنفيذ ترحيل قاعدة البيانات
4 php artisan config:cache تخزين التهيئة مؤقتًا
5 php artisan route:cache تخزين المسارات مؤقتًا
6 php artisan view:cache تخزين العروض مؤقتًا
7 php artisan storage:link إنشاء رابط التخزين
8 php artisan queue:restart إعادة تشغيل عمال الطابور
9 فحص الصلاحيات storage/ + bootstrap/cache/ قابلان للكتابة
10 فحص صحي /health يرجع 200

(1) ▶ مثال: نص نشر آلي لـ ShopMetrics

BASH
#!/bin/bash
# deploy.sh — نص نشر بدون توقف
set -e

APP_DIR="/var/www/shopmetrics"
RELEASES_DIR="/var/www/releases"
CURRENT_LINK="/var/www/current"
NEW_RELEASE=$(date +%Y%m%d%H%M%S)

echo "=== جاري نشر ShopMetrics ==="

# 1. إنشاء مجلد الإصدار
mkdir -p "$RELEASES_DIR/$NEW_RELEASE"
cd "$RELEASES_DIR/$NEW_RELEASE"

# 2. استنساخ/سحب الكود
git clone --depth 1 --branch main git@github.com:bob/shopmetrics.git .
echo "→ تم سحب الكود"

# 3. تثبيت التبعيات
composer install --no-dev --optimize-autoloader --no-progress
echo "→ تم تثبيت التبعيات"

# 4. نسخ ملف البيئة
cp "$APP_DIR/.env.production" .env

# 5. تحسين
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "→ تم بناء التخزين المؤقت"

# 6. تبديل الرابط الرمزي (ذري)
ln -sfn "$RELEASES_DIR/$NEW_RELEASE" "$CURRENT_LINK"
echo "→ تم تبديل الرابط الرمزي"

# 7. تشغيل الترحيلات
cd "$CURRENT_LINK"
php artisan migrate --force
echo "→ اكتملت الترحيلات"

# 8. إعادة تشغيل عمال الطابور
php artisan queue:restart
echo "→ جاري إعادة تشغيل عمال الطابور"

# 9. فحص صحي
sleep 3
if curl -sf http://localhost:8080/health > /dev/null; then
    echo "✓ اجتاز الفحص الصحي"
else
    echo "✗ فشل الفحص الصحي! جاري التراجع..."
    ln -sfn "$RELEASES_DIR/$(ls -t $RELEASES_DIR | sed -n '2p')" "$CURRENT_LINK"
    echo "✗ تم التراجع"
    exit 1
fi

# 10. تنظيف الإصدارات القديمة (الاحتفاظ بآخر 5)
ls -t "$RELEASES_DIR" | tail -n +6 | xargs -r rm -rf
echo "→ تم تنظيف الإصدارات القديمة"

echo "=== اكتمل النشر! ==="

الناتج:

TEXT
{"status":"ok","data":{}}

7. النشر بدون توقف

(1) مقارنة استراتيجيات النشر

الاستراتيجية وقت التوقف استخدام الموارد التعقيد حالات الاستخدام
نشر مباشر 5-30 ثانية منخفض منخفض حركة منخفضة/توقف قصير مقبول
أزرق-أخضر 0 مضاعف متوسط متطلبات توافر عالية
تحديث متدرج 0 1.5x متوسط نشر متعدد المثيلات
كناري 0 1.1x عالي واسع النطاق / يتطلب تحقق تدريجي

(2) النشر الأزرق-الأخضر

100%
flowchart LR
    subgraph Blue["الأزرق (الحالي)"]
        B1[التطبيق v1.0]
        B2[قاعدة البيانات v1.0]
    end
    subgraph Green["الأخضر (الجديد)"]
        G1[التطبيق v1.1]
        G2[قاعدة البيانات v1.1]
    end
    LB[موازن الحمل] -->|"100%"| B1
    LB -.->|"0%"| G1
    
    LB -->|"التبديل بعد الفحص الصحي"| G1
BASH
# نشر أزرق-أخضر باستخدام Docker
# الحالي: الأزرق نشط، الأخضر خامل
docker compose -f docker-compose.blue.yml up -d
# فحص صحي للأخضر
curl -sf http://green:8080/health || exit 1
# تبديل الحركة (تحديث upstream في nginx)
echo "server green:8080;" > /etc/nginx/conf.d/upstream.conf
nginx -s reload
# إيقاف الأزرق القديم
docker compose -f docker-compose.blue.yml down

(1) ▶ مثال: نشر بدون توقف عبر GitHub Actions في ShopMetrics

YAML
# .github/workflows/zero-downtime-deploy.yml
name: Zero-Downtime Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Determine active slot
        uses: appleboy/ssh-action@v1
        id: slot
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            if docker compose -f docker-compose.blue.yml ps | grep -q "Up"; then
              echo "ACTIVE=blue" >> $GITHUB_OUTPUT
              echo "DEPLOY=green" >> $GITHUB_OUTPUT
            else
              echo "ACTIVE=green" >> $GITHUB_OUTPUT
              echo "DEPLOY=blue" >> $GITHUB_OUTPUT
            fi

      - name: Deploy to inactive slot
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            DEPLOY=${{ steps.slot.outputs.DEPLOY }}
            docker compose -f docker-compose.${DEPLOY}.yml pull
            docker compose -f docker-compose.${DEPLOY}.yml up -d
            docker compose -f docker-compose.${DEPLOY}.yml exec app php artisan migrate --force
            docker compose -f docker-compose.${DEPLOY}.yml exec app php artisan config:cache

      - name: Health check & switch traffic
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: deploy
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            DEPLOY=${{ steps.slot.outputs.DEPLOY }}
            ACTIVE=${{ steps.slot.outputs.ACTIVE }}
            # انتظار الفحص الصحي
            for i in $(seq 1 10); do
              if curl -sf http://localhost:8080/health; then break; fi
              sleep 2
            done
            # تبديل upstream في nginx
            echo "server ${DEPLOY}:8080;" > /etc/nginx/conf.d/upstream.conf
            nginx -s reload
            # إيقاف الفتحة القديمة
            docker compose -f docker-compose.${ACTIVE}.yml down
            echo "Zero-downtime deploy complete!"

الناتج:

TEXT
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

8. مثال شامل: النشر والتهيئة الكامل لـ ShopMetrics

DOCKERFILE
# ============================================
# شامل: Dockerfile إنتاج ShopMetrics
# بناء متعدد المراحل مع التحسين
# ============================================

# المرحلة 1: بناء أصول الواجهة الأمامية
FROM node:20-alpine AS frontend
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY vite.config.ts tailwind.config.js postcss.config.js ./
COPY resources/ ./resources/
RUN npm run build

# المرحلة 2: تثبيت تبعيات PHP
FROM composer:2.7 AS backend
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-progress --classmap-authoritative
COPY . .
COPY --from=frontend /app/public/build ./public/build
RUN php artisan route:cache && \
    php artisan config:cache && \
    php artisan view:cache && \
    php artisan event:cache

# المرحلة 3: صورة الإنتاج
FROM php:8.3-fpm-alpine AS production
WORKDIR /var/www/html

RUN apk add --no-cache \
    curl nginx supervisor mysql-client \
    && docker-php-ext-install pdo_mysql opcache pcntl

COPY docker/opcache.ini /usr/local/etc/php/conf.d/
COPY docker/nginx.conf /etc/nginx/http.d/default.conf
COPY docker/supervisord.conf /etc/supervisord.conf
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

COPY --from=backend /app /var/www/html
RUN chown -R www-data:www-data storage bootstrap/cache

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

ENTRYPOINT ["/entrypoint.sh"]
BASH
#!/bin/bash
# docker/entrypoint.sh
set -e

echo "→ جاري تشغيل الترحيلات..."
php artisan migrate --force

echo "→ جاري بدء supervisord..."
exec /usr/bin/supervisord -c /etc/supervisord.conf
INI
; docker/supervisord.conf
[supervisord]
nodaemon=true
user=root

[program:php-fpm]
command=php-fpm --nodaemonize
autorestart=true

[program:nginx]
command=nginx -g 'daemon off;'
autorestart=true

[program:queue-worker]
command=php artisan queue:work --queue=high,default --sleep=3 --tries=3 --max-time=3600
autorestart=true
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d

❓ أسئلة شائعة

س أيهما أفضل، Docker أم النشر التقليدي؟
ج مزايا Docker تشمل بيئات متسقة ونشرًا قابلاً للتكرار وسهولة التوسع. عيوبه هي منحنى التعلم وعبء أداء طفيف (<5%). نوصي بـ Docker للمشاريع الجديدة؛ للمشاريع الحالية التي تستخدم النشر التقليدي، يُنصح بالهجرة التدريجية.
س كيف أدير شهادات SSL؟
ج استخدم Let's Encrypt + Certbot للتجديد التلقائي؛ أو عطّل SSL على المنصة السحابية (AWS ALB/Cloudflare) واستخدم HTTP فقط على الواجهة الخلفية. لا تدير الشهادات يدويًا.
س هل ترحيل قاعدة البيانات آمن في بيئة الإنتاج؟
ج إضافة أعمدة أو فهارس آمنة؛ حذف الأعمدة أو تغيير أنواعها يتطلب أولًا إضافة العمود الجديد → نقل البيانات → ثم حذف العمود القديم. استخدم php artisan migrate --force لأتمتة ذلك في CI؛ إذا فشل الترحيل، احظر النشر.
س ماذا يجب أن أفعل إذا كانت صورة Docker كبيرة جدًا؟
ج استخدم الصورة الأساسية Alpine، والبناء متعدد المراحل (بدون نسخ node_modules/.git)، وشغّل composer install مع --no-dev، واستخدم .dockerignore لاستبعاد ملفات الاختبار. الحجم المستهدف: < 200 ميغابايت.
س كيف يتعامل خط CI/CD مع التراجع؟
ج نحتفظ بآخر 5 إصدارات من صور Docker؛ للتراجع، ببساطة اجمع بين docker compose pull <previous-version> و docker compose up -d. التراجع أسرع مع النشر الأزرق-الأخضر — فقط ارجع إلى الفتحة القديمة.
س هل سيؤدي queue:restart إلى فقدان المهام؟
ج لا. يسمح queue:restart للعامل بالخروج بأمان بعد إكمال مهمته الحالية، ويعيد Supervisor تشغيل عامل جديد تلقائيًا. المهام التي لم تبدأ بعد تبقى في الطابور، بانتظار معالجتها من قبل العامل الجديد.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (⭐): أعد بيئة تطوير Docker Compose لـ ShopMetrics (app + MySQL + Redis + Mailpit) وتأكد من أن التطبيق قابل للوصول مباشرة بعد تشغيل docker compose up -d.

  2. تمرين متقدم (⭐⭐): اكتب سير عمل GitHub Actions يقوم تلقائيًا عند الدفع إلى الفرع الرئيسي بـ: تشغيل الاختبارات → بناء صورة Docker → دفع إلى السجل → النشر عبر SSH → فحص صحي. تضمين منطق حظر لفشل الاختبارات وفشل الفحص الصحي.

  3. تحدي (⭐⭐⭐): نفذ حل نشر أزرق-أخضر بدون توقف كامل: مجموعتان من تهيئات Docker Compose، اكتشاف تلقائي للفتحة النشطة، نشر إلى الفتحة غير النشطة، فحوصات صحية، تبديل upstream في Nginx، وإيقاف الفتحة القديمة بأمان. اكتب نص تراجع يمكنه العودة إلى الإصدار السابق خلال 30 ثانية.

Web-Tutorial.com

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

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

100%