دليل عملي لنشر Laravel في الإنتاج
النشر هو الخطوة الأخيرة لإطلاق المنتج للعالم — خطأ واحد قد يؤدي إلى رؤية جميع المستخدمين لصفحة خطأ 500.
1. ما ستتعلمه
- حاويات Docker: بناء متعدد المراحل وتنسيق Docker Compose
- تهيئة Nginx + PHP-FPM: وكيل عكسي وشهادات SSL
- خط أنابيب CI/CD: الاختبار الآلي والنشر باستخدام GitHub Actions
- قائمة فحص النشر: .env / config:cache / migrate / queue:restart
- استراتيجيات النشر بدون توقف: أزرق-أخضر / تحديث متدرج
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" واحد — الاختبار والبناء والنشر جميعها مؤتمتة بالكامل.
git push → GitHub Actions → اختبار → بناء Docker → نشر → فحص صحي → تم
(3) النتيجة
بعد أن نفذ Bob نظام CI/CD، انخفض وقت النشر من 40 دقيقة إلى 3 دقائق، وانخفض معدل الخطأ من 10% إلى 0%, ولم يعد يحتاج للدخول عبر SSH إلى الخادم في منتصف الليل.
3. حاويات Docker
(1) 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
# 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
# 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:
الناتج:
CONTAINER ID IMAGE STATUS PORTS
abc123 nginx:latest Up 2 hours 0.0.0.0:80->80/tcp
4. تهيئة Nginx + PHP-FPM
(1) تهيئة 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)
# إنهاء 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
# توجيه النطاقات الفرعية للمستأجرين
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;
}
}
الناتج:
// تم التنفيذ بنجاح
5. خط أنابيب CI/CD
(1) سير عمل GitHub Actions
# .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) مخطط تدفق النشر
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
// 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'),
]);
});
الناتج:
// تم التنفيذ بنجاح
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
#!/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 "=== اكتمل النشر! ==="
الناتج:
{"status":"ok","data":{}}
7. النشر بدون توقف
(1) مقارنة استراتيجيات النشر
| الاستراتيجية | وقت التوقف | استخدام الموارد | التعقيد | حالات الاستخدام |
|---|---|---|---|---|
| نشر مباشر | 5-30 ثانية | منخفض | منخفض | حركة منخفضة/توقف قصير مقبول |
| أزرق-أخضر | 0 | مضاعف | متوسط | متطلبات توافر عالية |
| تحديث متدرج | 0 | 1.5x | متوسط | نشر متعدد المثيلات |
| كناري | 0 | 1.1x | عالي | واسع النطاق / يتطلب تحقق تدريجي |
(2) النشر الأزرق-الأخضر
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
# نشر أزرق-أخضر باستخدام 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
# .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!"
الناتج:
CONTAINER ID IMAGE STATUS PORTS
abc123 nginx:latest Up 2 hours 0.0.0.0:80->80/tcp
8. مثال شامل: النشر والتهيئة الكامل لـ ShopMetrics
# ============================================
# شامل: 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"]
#!/bin/bash
# docker/entrypoint.sh
set -e
echo "→ جاري تشغيل الترحيلات..."
php artisan migrate --force
echo "→ جاري بدء supervisord..."
exec /usr/bin/supervisord -c /etc/supervisord.conf
; 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
❓ أسئلة شائعة
php artisan migrate --force لأتمتة ذلك في CI؛ إذا فشل الترحيل، احظر النشر.node_modules/.git)، وشغّل composer install مع --no-dev، واستخدم .dockerignore لاستبعاد ملفات الاختبار. الحجم المستهدف: < 200 ميغابايت.docker compose pull <previous-version> و docker compose up -d. التراجع أسرع مع النشر الأزرق-الأخضر — فقط ارجع إلى الفتحة القديمة.queue:restart إلى فقدان المهام؟queue:restart للعامل بالخروج بأمان بعد إكمال مهمته الحالية، ويعيد Supervisor تشغيل عامل جديد تلقائيًا. المهام التي لم تبدأ بعد تبقى في الطابور، بانتظار معالجتها من قبل العامل الجديد.📖 ملخص
- البناء متعدد المراحل في Docker يجعل الصور أخف ويضمن بيئات متسقة
- استخدم Docker Compose لتنسيق الخدمات الخمس: app، عامل، مجدول، mysql، و redis
- وكيل Nginx العكسي + إنهاء SSL + تهيئة رؤوس الأمان
- GitHub Actions CI/CD: اختبار → بناء → نشر مؤتمت بالكامل
- قائمة فحص النشر من 10 خطوات: لا تتخطى خطوة واحدة
- النشر الأزرق-الأخضر بدون توقف: أولًا، أطلق الإصدار الجديد → نفذ فحصًا صحيًا → بدّل الحركة
📝 تمارين
-
تمرين أساسي (⭐): أعد بيئة تطوير Docker Compose لـ ShopMetrics (app + MySQL + Redis + Mailpit) وتأكد من أن التطبيق قابل للوصول مباشرة بعد تشغيل
docker compose up -d. -
تمرين متقدم (⭐⭐): اكتب سير عمل GitHub Actions يقوم تلقائيًا عند الدفع إلى الفرع الرئيسي بـ: تشغيل الاختبارات → بناء صورة Docker → دفع إلى السجل → النشر عبر SSH → فحص صحي. تضمين منطق حظر لفشل الاختبارات وفشل الفحص الصحي.
-
تحدي (⭐⭐⭐): نفذ حل نشر أزرق-أخضر بدون توقف كامل: مجموعتان من تهيئات Docker Compose، اكتشاف تلقائي للفتحة النشطة، نشر إلى الفتحة غير النشطة، فحوصات صحية، تبديل upstream في Nginx، وإيقاف الفتحة القديمة بأمان. اكتب نص تراجع يمكنه العودة إلى الإصدار السابق خلال 30 ثانية.



