إنتقل إلى المحتوى الرئيسي

نشر بيئة الإنتاج

الهدف

بيئة الإنتاج المعتمدة الآن مبنية على:

  • VPS واحد أو أكثر يعمل بنظام Linux
  • Docker CE + Docker Compose Plugin
  • Supabase self-hosted داخل نفس البنية
  • Traefik كـ reverse proxy و TLS terminator
  • PostgreSQL + Redis + Kong + GoTrue + PostgREST + Realtime + Storage
  • خدمات ذاتية الاستضافة مثل ntfy و MinIO و GlitchTip و Grafana

لا يوجد اعتماد تشغيلي مطلوب على Supabase Cloud من أجل النشر الإنتاجي.

baseline خدمات البيانات

الـ stack المعتمد مثبت ببصمات OCI لا بوسوم عائمة:

المكوّنالإصدار
PostgreSQL15.14.1.116
Kong3.9.3
GoTrue2.189.0
PostgREST14.12
Realtime2.102.3
Storage API1.60.4
Postgres Meta0.96.6
Supabase Studio2026.08.03-sha-022b374
Authentik2026.5.6

يبقى PostgreSQL على major 15 عمداً. لا تُطبق صورة PG17 التي تظهر في stack Supabase الجديد على volume الحالي؛ استخدم runbook ترقية PostgreSQL المنفصل.

النطاقات المقترحة

  • shambus.com للعميل
  • dashboard.shambus.com للوحة الشركات
  • admin.shambus.com للإدارة
  • docs.shambus.com للتوثيق
  • api.shambus.com لبوابة Supabase/Kong
  • studio.shambus.com لـ Supabase Studio
  • grafana.shambus.com للمراقبة
  • errors.shambus.com لـ GlitchTip
  • ntfy.shambus.com للتنبيهات
  • minio-console.shambus.com لإدارة التخزين
  • auth.shambus.com للمصادقة الموحدة على أدوات التشغيل
  • demo.shambus.com لمركز العرض وتجربة المسافر الخاصة
  • demo-dashboard.shambus.com للوحة مدير الشركة التجريبية
  • demo-driver.shambus.com لتطبيق السائق التجريبي
  • web-api.shambus.com لمسارات API التي تحتاجها تطبيقات الجوال مع إغلاق البوابة العامة

متطلبات الخادم

الحد الأدنى العملي:

  • 4 vCPU
  • 8 GB RAM
  • 120 GB SSD
  • Ubuntu 24.04 LTS أو 22.04 LTS

الموصى به عند تشغيل كل الخدمات الذاتية الاستضافة على نفس الخادم:

  • 8 vCPU
  • 16 GB RAM
  • 250 GB SSD

تثبيت Docker CE

sudo apt update
sudo apt install -y ca-certificates curl gnupg lsb-release

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"

أعد تسجيل الدخول بعد إضافة المستخدم إلى مجموعة docker.

المتغيرات البيئية

انسخ ملف البيئة:

cp .env.prod.example .env.prod

ثم املأ القيم الفعلية في .env.prod. أهم المفاتيح:

  • DOMAIN
  • ACME_EMAIL
  • POSTGRES_PASSWORD
  • JWT_SECRET
  • SECRET_KEY_BASE (مستقل، 64 محرفاً على الأقل)
  • REALTIME_DB_ENC_KEY (16 محرفاً تماماً)
  • PG_META_CRYPTO_KEY (32 محرفاً على الأقل)
  • SUPABASE_ANON_KEY
  • SUPABASE_SERVICE_ROLE_KEY
  • TWILIO_MESSAGE_SERVICE_SID
  • TRAEFIK_DASHBOARD_AUTH
  • GRAFANA_ADMIN_PASSWORD
  • GLITCHTIP_SECRET_KEY
  • GLITCHTIP_POSTGRES_PASSWORD
  • MINIO_ROOT_PASSWORD
  • GOOGLE_MAPS_API_KEY_WEB (اختياري، مقيّد بنطاقات الويب)
  • GOOGLE_MAPS_API_KEY_ANDROID (اختياري، مقيّد بتطبيقات Android)
  • GOOGLE_MAPS_API_KEY_IOS (اختياري، مقيّد بحزم iOS)
  • SMTP_*
  • CUSTOMER_GLITCHTIP_DSN
  • DASHBOARD_GLITCHTIP_DSN
  • ADMIN_GLITCHTIP_DSN
  • MOBILE_CUSTOMER_GLITCHTIP_DSN
  • MOBILE_DRIVER_GLITCHTIP_DSN
  • GOOGLE_OAUTH_WEB_CLIENT_ID (جمهور التحقق على الخادم وserver client للموبايل)
  • GOOGLE_OAUTH_IOS_CLIENT_ID
  • GOOGLE_OAUTH_IOS_REVERSED_CLIENT_ID
  • TWILIO_*
  • NEXT_PUBLIC_VAPID_PUBLIC_KEY
  • VAPID_PRIVATE_KEY
  • VAPID_SUBJECT
  • DEMO_ACCESS_SECRET
  • CRON_SECRET
  • DEMO_HUB_URL
  • DEMO_DASHBOARD_URL
  • DEMO_DRIVER_URL
  • WEB_API_URL
  • PUBLIC_SITE_MODE
  • MAINTENANCE_INTERVAL_SECONDS
  • PGRST_DB_SCHEMAS=public
  • PGRST_DB_MAX_ROWS=1000

يجب توليد مفاتيح VAPID مستقلة للإنتاج واستخدامها لكل تطبيق ويب يحتاج إشعارات متصفح. لا تستخدم مفاتيح التطوير الافتراضية الموجودة في docker-compose.yml؛ هذه مخصصة فقط لتجربة push notifications على localhost.

معرّفات OAuth الثلاثة عامة لكنها إلزامية لدخول المسافر عبر Google. سجل Web client واحداً للجمهور الذي تتحقق منه Customer API، وiOS client للحزمة sy.shambus.app، وAndroid client للحزمة نفسها مع SHA-1 لمفتاح التوقيع الفعلي. استخدم SHA-256 أيضاً عند تقييد مفاتيح Maps أو خدمات Google التي تطلبه، لكنه ليس بديلاً عن SHA-1 في تعريف Android OAuth. لا تضف Google client secret إلى Flutter أو Docker Compose. يبني النشر Android دائماً بنكهة prod ويمرر إعداد iOS إلى URL scheme مؤقت غير متعقب.

أضف إلى Web OAuth client أصول JavaScript المصرح بها https://shambus.com وhttps://www.shambus.com وhttps://app.shambus.com، وأضف أصلي localhost:3000 وlocalhost:8180 فقط إلى client تطوير منفصل عند الحاجة. يعرض كل من Customer Web وFlutter Web/PWA زر Google Identity Services الرسمي ويرسل ID token إلى Customer API؛ لا يعتمد أي منهما callback redirect من GoTrue. يجب أن يطابق Android client بصمات مفتاح الإصدار الفعلي، لا debug key، قبل بناء حزمة الإنتاج.

أنشئ العملاء واضبط أماكنهم من خلال المعالج القابل لإعادة التشغيل:

./infrastructure/scripts/configure-google-traveler-oauth.sh

يفتح المعالج صفحات Google Auth Platform المطلوبة ويوجّه المشغّل عبر Branding وAudience وعميلَي Web المنفصلين للإنتاج والتطوير وعملاء Android وiOS. تحفظ الحالة العامة فقط في .env.google-oauth المتجاهل من Git، ويضبط Web client المحلي في apps/customer/.env.local. كما يحاول ضبط GitHub repository variables الثلاثة، ويعرض تحديث /opt/shambus/.env.prod عبر SSH بعد إنشاء نسخة احتياطية مؤرخة. لا يطلب المعالج client secret لأن التدفق لا يحتاج إليه.

يتطلب مسار .github/workflows/deploy-mobile.yml المتغيرات العامة التالية:

  • GOOGLE_OAUTH_WEB_CLIENT_ID
  • GOOGLE_OAUTH_IOS_CLIENT_ID
  • GOOGLE_OAUTH_IOS_REVERSED_CLIENT_ID
  • WEB_API_URL وNTFY_BASE_URL عند تجاوز القيم الإنتاجية الافتراضية

وتبقى مفاتيح Supabase وشهادات التوقيع وDSN ومفاتيح المتاجر في GitHub Secrets. يفشل workflow قبل البناء إذا غابت هوية Google المطلوبة لتطبيق العميل أو أشارت واجهات الإنتاج إلى localhost. كما يستخدم معرف الحزمة الفعلي sy.shambus.app عند رفع AAB؛ لا تستخدم الهوية القديمة sy.shambus.customer.

يتطلب بناء iOS في GitHub أيضاً APPLE_TEAM_ID وملفَي provisioning منفصلين: IOS_CUSTOMER_PROVISIONING_PROFILE_BASE64 و IOS_DRIVER_PROVISIONING_PROFILE_BASE64. يبقى IOS_PROVISIONING_PROFILE_BASE64 توافقاً مؤقتاً لتطبيق العميل فقط. يفك المسار الملف دون طباعته، ويتحقق من Team ID وBundle ID، ثم يولد ExportOptions مؤقتاً؛ لا تعتمد على ملف يحمل YOUR_TEAM_ID أو على profile واحد للتطبيقين.

تبقى مفاتيح الخرائط قابلة للرؤية داخل تطبيق العميل، لذلك لا تستخدم مفتاح الخادم فيها. يقتصر مفتاح الويب على app وdriver وdemo-driver عبر HTTP referrers، ويقيد مفتاحا Android وiOS بمعرفات التطبيقات وبصمات التوقيع المناسبة. عند غياب المفتاح لا تظهر خريطة فارغة: تبقى بيانات GPS والحالة المباشرة عاملة، وتوفر الواجهة زر فتح المسار أو موقع الحافلة في تطبيق خرائط خارجي دون استهلاك Maps JavaScript API.

تعامل سكربتات النشر والمصالحة .env.prod كملف بيانات ولا تنفذه كـ shell. مع ذلك، ضع القيم التي تحتوي مسافات أو # بين علامتي اقتباس، مثلاً:

INTERNAL_QA_PLATFORM_ADMIN_NAME="مشرف المنصة QA"

يقوم configure-production-qa-env.sh وconfigure-demo-production-env.sh بالاقتباس تلقائياً. يمكن التحقق من الملف دون تغيير الحاويات عبر:

SHAMBUS_ENV_VALIDATE_ONLY=true \
./infrastructure/scripts/reconcile-production-stack.sh

يضبط configure-demo-production-env.sh أيضاً عقد OTP الآمن للعرض الخاص. إذا لم يكن هناك مزود تحقق معتمد، يختار OTP_PROVIDER=disabled ويولد سراً مستقلاً لتجزئة تحديات العرض ورمزاً من ستة أرقام لحسابات +963900… فقط. لا تُرسل أرقام العملاء الحقيقية إلى Meta أو إلى أي مزود آخر في هذا الوضع. كما يفعّل البريد أو Web Push تلقائياً فقط عندما تكون بيانات المزود مكتملة، ويترك SMS وWhatsApp التشغيليين معطلين افتراضياً. يحافظ السكربت على أي مزود twilio أو external محدد صراحةً، وينشئ نسخة احتياطية محمية ويثبت صلاحية .env.prod على 0600.

  • CERTIFICATE_PINS

ما الذي يجعل مسار الإنتاج مختلفاً عن التطوير؟

  • ملف docker-compose.prod.yml يعطّل المنافذ المحلية الموروثة من التطوير بحيث يبقى النشر العام عبر Traefik فقط.
  • خدمات الويب (customer, dashboard, admin, docs) لا تستخدم bind mounts في الإنتاج؛ يتم تشغيل الصور المبنية فقط.
  • سكربت deploy-production.sh server لم يعد يعتمد على sleep 20، بل ينتظر صحة الخدمات فعلياً ثم ينفذ smoke checks داخل الحاويات.
  • مسار بناء الجوال الأصلي أصبح يعتمد على التطبيقات الحقيقية تحت apps/mobile/customer و apps/mobile/driver.
  • تهيئة حسابات PostgreSQL لا تعيد منح صلاحيات واسعة للجداول الموجودة عند كل إقلاع؛ تبقى قيود الأعمدة التي فرضتها الترحيلات الأمنية محفوظة، وتقرأ دوال auth.uid() وauth.role() وauth.email() حمولة JWT بصيغة JSON التي يستخدمها PostgREST الحديث.

نموذج النشر

1. بناء الصور

يجب أن يبقى ملف .dockerignore في جذر المستودع فعالاً عند البناء. يستبعد الملف أسرار .env*، وحالة Git والنشر، ومخرجات الحزم، وسجلات التشغيل، وملفات macOS من نوع ._*. لا تنقل أرشيفاً إلى الخادم قبل فحصه من هذه الملفات، ولا تضف ملف البيئة الإنتاجي إلى Docker build context حتى لو لم ينسخه Dockerfile صراحة.

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod \
build minio customer dashboard admin docs mobile-customer mobile-driver

يبني minio الإصدار الأمني المفتوح المصدر الأخير RELEASE.2025-10-15T17-29-55Z من الوسم الرسمي نفسه، ويتحقق من commit المحدد قبل البناء لأن هذا الإصدار لم يُنشر كصورة registry رسمية. صور البناء وruntime مثبتة ببصمات manifest، كما أن عميل mc مثبت بإصدار وبصمة محددين؛ لا تستبدلها بوسم latest.

2. تشغيل البنية الأساسية أولاً

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod up -d \
db redis auth rest realtime imgproxy storage kong meta studio \
traefik pgbouncer backup minio minio-init backup-sync ntfy \
loki alloy tempo prometheus alertmanager grafana \
glitchtip-db glitchtip-redis glitchtip glitchtip-worker

يجمع Grafana Alloy سجلات Docker ويرسلها إلى Loki. حلّ محل Promtail بعد انتهاء دعمه في 2 مارس 2026، ويحتفظ بمؤشرات القراءة داخل volume باسم shambus-alloy-data حتى لا تعيد إعادة إنشاء الحاوية إرسال السجل التاريخي كله. يسقط Alloy محلياً الأسطر الأقدم من نافذة قبول Loki البالغة سبعة أيام ويسجلها في metric صريح، بدلاً من إرسال بيانات يعرف مسبقاً أن Loki سيرفضها. صورة Alloy مثبتة على الإصدار 1.16.2 وبصمة OCI محددة.

كل خدمة طويلة العمر في profile الإنتاج تملك healthcheck وظيفياً، بما فيها Traefik وPgBouncer وTempo وGrafana وGlitchTip worker وbackup-sync وبوت Telegram ومهمة الصيانة. أما db-bootstrap وdb-startup وminio-init فهي مهام one-shot ويُعد خروجها بالرمز 0 الحالة الصحيحة، لا بقاءها في وضع running.

لا يبدأ Kong قبل أن يصبح Realtime healthy. هذا الاعتماد جزء من عقد الإقلاع، لأن قبول بوابة HTTP قبل جاهزية ترقية WebSocket قد ينتج 503 متقطعاً في أول اشتراك. تبقى واجهة حجز العميل متسامحة مع انقطاع عابر: تعرض حالة إعادة الاتصال وتحدث الحجز عبر API دورياً حتى يعود الاشتراك؛ لكنها لا تخفي انقطاعاً مستمراً عن اختبارات المتصفح أو المراقبة.

يتضمن هذا المسار أيضاً خدمة db-bootstrap بشكل غير مباشر عبر الاعتماديات حتى تتم:

  • مزامنة كلمات مرور حسابات Supabase الداخلية
  • تطبيق الترحيلات المطلوبة
  • تطبيق seed مرجعي دائم مثل المدن السورية وصلاحيات الإدارة

3. تشغيل تهيئة قاعدة البيانات للإنتاج

./infrastructure/scripts/validate-migrations.sh
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod up -d db-startup

خدمة db-startup تنفذ مهمة إضافية قبل صعود التطبيقات:

  • تطبيق seed داخلي محدود للحسابات التشغيلية إذا كان ENABLE_INTERNAL_PROD_SEEDING=true

هذا المسار لا يستخدم infrastructure/supabase/seed.sql الشامل الخاص بالديمو/التطوير.

4. تشغيل التطبيقات

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod up -d \
customer dashboard admin docs mobile-customer mobile-driver

5. أو استخدم سكربت النشر

./infrastructure/scripts/deploy-production.sh server

للتحديثات الجزئية استخدم النشر عديم الانقطاع مع أسماء الخدمات المطلوبة فقط:

./infrastructure/scripts/deploy-production.sh apps dashboard docs

حذف الأسماء يبني وينشر جميع تطبيقات الويب والموبايل، أما تحديدها فيمنع إعادة بناء صور لم تتغير ويحافظ على نفس health check وDocker rollout لكل خدمة.

السكربت الآن يقوم بالتالي تلقائياً:

  • يتحقق من متغيرات الإنتاج الحرجة
  • يتحقق من صحة دمج docker-compose.yml مع docker-compose.prod.yml
  • يبني الصور الإنتاجية
  • ينتظر الخدمات حتى تصبح healthy أو running فعلياً
  • ينتظر اكتمال db-startup الذي يطبق الترحيلات وseed الداخلي عند تفعيله
  • يعيد صلاحيات القراءة إلى ملفات الإعدادات غير السرية المثبتة داخل حاويات غير جذرية، مع إبقاء .env.prod وملف Kong المولّد وملفات Webhook مقيدة.
  • بعد صعود نسختي العميل والإدارة الجديدتين يشغل backfill قابل لإعادة التشغيل لتحويل أسرار TOTP القديمة إلى أغلفة totp:v1 مشفرة، ويفشل النشر إذا بقي صف غير صالح أو تعذر تحديثه
  • ينفذ smoke checks على:
    • customer /api/health
    • dashboard /api/health
    • admin /api/health
    • docs /health
    • mobile-customer /health
    • mobile-driver /health
  • يشغل maintenance-cron لتنظيف العروض المنتهية كل 15 دقيقة افتراضياً
  • يبقي coming-soon على النطاق العام عندما تكون PUBLIC_SITE_MODE=coming-soon

لا تضف container_name ثابتاً إلى coming-soon أو خدمات الويب. يعتمد مسار النشر عديم الانقطاع على إنشاء مثيل ثانٍ باسم يولده Compose، وانتظار صحته، ثم إزالة المثيل السابق.

seed داخلي للحسابات التشغيلية

إذا كنت تحتاج حسابات داخلية ثابتة في الإنتاج للاختبارات التشغيلية أو للوصول الأولي، فعّل:

ENABLE_INTERNAL_PROD_SEEDING=true
INTERNAL_ADMIN_EMAIL=internal-admin@shambus.com
INTERNAL_ADMIN_PASSWORD=<strong-password>
INTERNAL_QA_COMPANY_NAME_AR=شركة شام باص الداخلية
INTERNAL_QA_COMPANY_PHONE=+963900000001
INTERNAL_QA_COMPANY_OWNER_EMAIL=internal-owner@shambus.com
INTERNAL_QA_COMPANY_OWNER_PASSWORD=<strong-password>
INTERNAL_QA_SEED_DAYS=14
INTERNAL_QA_SEED_BOOKINGS_PER_TRIP=3

ويمكنك إضافة حسابات اختيارية أيضاً:

  • INTERNAL_QA_PASSENGER_* لحساب عميل داخلي يعتمد على OTP
  • INTERNAL_QA_DRIVER_* لحساب سائق داخلي مرتبط بالشركة الداخلية

الseed الداخلي قابل لإعادة التشغيل: يحدّث كلمات المرور والربط بين الحسابات، يتحقق من تخطيطات الباصات، ويحافظ على عالم QA المكتمل بدلاً من إضافة حجوزات ومراجعات جديدة عند كل restart. يمكن خفض حجم البيانات في بيئات العقود عبر المتغيرين أعلاه، بينما يبقى افتراض الإنتاج 14 يوماً و3 حجوزات لكل رحلة مختارة.

بناء تطبيقات الجوال الأصلية

تستخدم صور الويب لتطبيقي الجوال أرشيف Flutter الرسمي المثبت على الإصدار 3.38.5 من storage.googleapis.com، وتتحقق من بصمة SHA-256 قبل فكّه. عند ترقية Flutter يجب تحديث FLUTTER_VERSION وFLUTTER_SHA256 معاً في apps/mobile/customer/Dockerfile.prod وapps/mobile/driver/Dockerfile.prod، ثم إعادة توليد ملفات القفل وتشغيل اختبارات التطبيقين. لا تُحوّل بناء الإنتاج إلى صورة registry عائمة مثل stable لأن ذلك يجعل أداة البناء غير قابلة لإعادة الإنتاج.

يعتمد التطبيقان أيضاً على الحزمتين المشتركتين packages/mobile_core و packages/mobile_ui. لذلك يجب أن يبقى build.context للخدمتين mobile-customer وmobile-driver مضبوطاً على جذر المستودع، بينما يشير dockerfile إلى ملف التطبيق. تنسخ ملفات Docker بيانات pubspec للحزم المشتركة قبل flutter pub get للاستفادة من cache، ثم تنسخ مصادر الحزم قبل البناء. يغطي اختبار production-infrastructure-contract.test.mjs هذا العقد لتفادي فشل النشر بسبب حزمة محلية غير موجودة.

يمكنك الآن اختيار التطبيق المطلوب صراحة:

./infrastructure/scripts/deploy-production.sh ios customer
./infrastructure/scripts/deploy-production.sh ios driver
./infrastructure/scripts/deploy-production.sh ios all

./infrastructure/scripts/deploy-production.sh android customer
./infrastructure/scripts/deploy-production.sh android driver
./infrastructure/scripts/deploy-production.sh android all

السكربت يمرر تلقائياً:

  • SUPABASE_URL
  • SUPABASE_ANON_KEY
  • WEB_API_URL
  • NTFY_URL
  • CERTIFICATE_PINS
  • ENVIRONMENT=production
  • GOOGLE_MAPS_API_KEY من المفتاح المقيّد بالمنصة، إن وجد
  • GLITCHTIP_DSN من مشروع التطبيق المحدد

في Android يمرر السكربت المفتاح نفسه إلى manifest placeholder، وفي iOS ينشئ ios/Flutter/Maps.xcconfig مؤقتاً بصلاحية مقيدة ويحذفه فور انتهاء البناء. لا تضف ملف المفاتيح الناتج إلى Git. يبقى المفتاح اختيارياً في التطبيقين بسبب مسار الرجوع إلى الخرائط الخارجية.

الترحيلات النظيفة

قبل أي نشر:

  • شغّل ./infrastructure/scripts/validate-migrations.sh
  • لا يُسمح بوجود رقم ترحيل مكرر
  • كل ملف يجب أن يكون بصيغة 00001_name.sql
  • التشغيل يتم بترتيب lexicographic ثابت
  • حالة التطبيق تُسجل في جدول public.applied_migrations
  • لا تعدّل ملفاً سبق تطبيقه. عند اكتشاف checksum تاريخي معروف، أضف ترحيلاً تصحيحياً يعيد تثبيت الحالة النهائية ثم طبّع ذلك الانتقال الدقيق فقط؛ يبقى أي اختلاف آخر سبباً لإيقاف النشر.
  • الترحيل 00198 يثبت أيضاً default privileges للدوال؛ بعد أي تعديل على bootstrap شغّل اختبار rls-security.test.ts مرة بعد الترحيل ومرة بعد إعادة تشغيل db-bootstrap

هذا يمنع حالة “قاعدة نصف مهاجرة” عند أول تشغيل أو أثناء التحديثات.

الخدمات الذاتية الاستضافة

الخدمات المجهزة في المسار الإنتاجي:

  • PostgreSQL
  • Redis
  • Kong
  • GoTrue
  • PostgREST
  • Realtime
  • Storage API
  • Imgproxy
  • Supabase Studio + Postgres Meta
  • Traefik
  • pgBouncer
  • MinIO
  • ntfy
  • Prometheus
  • Loki
  • Promtail
  • Tempo
  • Grafana
  • GlitchTip

تهيئة GlitchTip والتحقق منه

بعد تعبئة SMTP وتشغيل GlitchTip، أنشئ المؤسسة والمشاريع والتنبيهات ومراقبات الصحة المتباعدة خمس دقائق، ثم نفذ فحصاً حقيقياً من ingress العام حتى البريد:

./infrastructure/scripts/configure-glitchtip-production.sh
./infrastructure/scripts/verify-glitchtip-production.sh

السكربت الأول idempotent ويكتب DSNs الخمسة مباشرة إلى .env.prod المحمي من دون طباعتها. ينشئ مشاريع منفصلة للعميل ولوحة الشركة والإدارة وتطبيقي Flutter، إضافة إلى مشروع uptime وسبع مراقبات. السكربت الثاني يرسل خطأ اصطناعياً موسوماً بوضوح، ويتحقق من قبول public store endpoint، وحفظ الحدث، ومعالجته في worker، وإرسال تنبيه SMTP. احذف الحدث الاصطناعي من الواجهة لاحقاً إذا لم تعد تحتاجه كأثر قبول.

عند تفعيل Slack، يضيف سكربت التهيئة نفسه مستلم GENERAL_WEBHOOK متوافقاً مع Slack لكل قواعد GlitchTip من ملف bugs.url المحمي. يستخدم Alertmanager ملفي bugs.url وoperations.url مباشرة عبر api_url_file ويبقي ntfy قناة احتياطية. راجع دليل إشعارات Slack لإعداد الملفات، التوجيه، تدوير الروابط، واختبار التسليم الحقيقي من دون إظهار الأسرار.

تبقى واجهة errors.shambus.com وكل API الإداري خلف Authentik. الاستثناء العام محصور في مسارات ingest المتوافقة مع Sentry (store, envelope, security, minidump, unreal) مع rate limit وحجم body محدودين؛ لا توسّع الاستثناء إلى /api/*. يجب استخدام STARTTLS على منفذ 587، لأن 465 هو implicit TLS وقد يكون محجوباً من مزود الـ VPS أو غير متوافق مع إعداد Django الحالي.

يحتفظ Prometheus بحد زمني قدره 15 يوماً وحد حجم قدره 10 GB؛ يُطبق الحد الذي يصل إليه أولاً. يعتمد تنبيه PrometheusStorageHigh على حد الحجم ويعمل فقط عندما تكون قيمة prometheus_tsdb_retention_limit_bytes موجبة، حتى لا يتحول غياب حد الحجم إلى قسمة على صفر وتنبيه دائم كاذب.

عند ترقية طبقة Supabase/Kong لا تستبدل الخدمات كلها دفعة واحدة. يجب أخذ dump يحفظ المالكين والصلاحيات، وتشغيل الصور المرشحة على clone مع شبكة داخلية، ثم نشر meta/studio وrest/kong وauth/realtime/storage على مراحل. الإجراء الكامل وعقود 200/401/404 موثقة في دليل ترقية Supabase وKong.

المنافذ العامة

المنافذ التي يجب كشفها على الـ VPS:

  • 80/tcp
  • 443/tcp

لا تكشف 5432 أو 6379 أو منافذ Grafana/Studio/MinIO أو تطبيقات الويب مباشرة للإنترنت. الوصول العام يجب أن يمر عبر Traefik فقط. يبقى S3 API الخاص بـ MinIO على minio:9000 داخل شبكة Docker بلا DNS أو router عام؛ وحدها واجهة minio-console المحمية ببيانات MinIO تمر عبر Traefik. كذلك تبقى لوحة Traefik بلا DNS أو router عام ويجري التشغيل عبر السجلات والمقاييس وأوامر Docker.

التحقق بعد النشر

docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod ps
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod logs -f kong
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod logs -f customer

تحقق من:

  • https://shambus.com
  • https://dashboard.shambus.com
  • https://admin.shambus.com
  • https://api.shambus.com
  • https://studio.shambus.com
  • https://grafana.shambus.com
  • https://errors.shambus.com
  • https://demo.shambus.com/demo
  • https://demo-dashboard.shambus.com/demo/access
  • https://demo-driver.shambus.com
  • https://web-api.shambus.com/api/health

تفعيل صفحة «قريباً» العامة

توجد صفحة إطلاق ثابتة في apps/coming-soon. تعمل كخدمة اختيارية صغيرة، وتستخدم مسار Traefik بأولوية أعلى للنطاقين shambus.com وwww.shambus.com. تبقى خدمة customer قيد التشغيل وبصحة جيدة، ولا تتأثر نطاقات لوحة الشركات أو الإدارة أو العروض. تستخدم تطبيقات الجوال web-api.${DOMAIN} بدلاً من النطاق العام، لذلك لا تكسر صفحة الإطلاق طلبات API.

تنسخ صورة البطل المتجاوبة من apps/coming-soon/images إلى الحاوية نفسها، ولا تجلب الصفحة خطاً أو صورة أو سكربتاً من طرف خارجي. يجب أن يستمر فحص النشر في التحقق من Content-Security-Policy، ومن استجابة أحجام WebP، ومن عدم وجود overflow على 390px. زر «دخول الشركات» يفتح لوحة الشركات، بينما يظل إجراء العرض المخصص رابط تواصل ولا يفتح بوابة الركاب العامة.

يحمي Traefik نطاقي demo وweb-api بحد مصدر قدره 20 طلباً/ثانية مع burst مقداره 60، وتطبق نقطة بحث الرحلات حداً إضافياً قدره 100 طلب/دقيقة لكل IP. تبقى استدعاءات Next.js الموثوقة إلى PostgREST معرفة في Kong باسم consumer service_role وبميزانية منفصلة (250 طلباً/ثانية)، حتى لا تؤدي ذروة عامة أو تزامن تطبيقات الإدارة والصيانة إلى 429 داخل رحلة مستخدم صحيحة. لا يغير override حدود العميل anon: يظل حد REST العام 20 طلباً/ثانية و500 طلب/دقيقة لكل مصدر.

يجب أن تضبط إضافات Kong العامة وauth-v1-user وauth-v1 وrest-v1 limit_by: ip صراحةً. القيمة الافتراضية consumer تجمع كل طلب يحمل مفتاح Supabase العام تحت consumer واحد اسمه anon، فتحوّل حد Auth البالغ 5 طلبات/ثانية إلى سقف مشترك للمنصة كلها. يحل Kong عنوان العميل من سلسلة X-Forwarded-For التي ينشئها Traefik، مع الوثوق فقط بنطاقات الشبكة الخاصة التي يمكنها الوصول إلى Kong؛ لا تفعّل ثقة forwarded headers القادمة مباشرة من الإنترنت. يبقى override الخاص بـservice_role وحده مضبوطاً على limit_by: consumer عمداً لأنه يمثل حركة خادم موثوقة ذات ميزانية مستقلة.

نشر نطاقات العرض الخاص

أضف سجلات A للأسماء demo وdemo-dashboard وdemo-driver وweb-api إلى عنوان VPS نفسه. يعرّف docker-compose.prod.yml مسارات Traefik وشهادات TLS تلقائياً، ويضيف X-Robots-Tag: noindex, nofollow وCache-Control: no-store لمسارات العرض.

لا تجعل صفحة مركز العرض متاحة على النطاق العام كحل بديل؛ الفصل بالنطاق هو جزء من التحكم في التخزين المؤقت والفهرسة وسياسة الوصول.

تعتمد بوابة demo.shambus.com على ترويسة Host الأصلية التي يحافظ عليها Traefik، وليس على اسم upstream الداخلي الذي قد يعيد Next.js بناءه داخل request.nextUrl، ولا على X-Forwarded-Host القابلة للانتحال من العميل. لذلك يجب أن يعيد طلب صفحة بلا capability توجيهاً إلى /demo، وأن تعيد نقطة API خاصة بلا capability الحالة 401:

curl -sS -o /dev/null -D - https://demo.shambus.com/trips
curl -sS -o /dev/null -w '%{http_code}\n' https://demo.shambus.com/api/trips

لتفعيل الصفحة:

docker compose \
--env-file .env.prod \
-f docker-compose.yml \
-f docker-compose.prod.yml \
--profile coming-soon \
up -d --build coming-soon

تحقق بعدها من صحة الحاوية ومن استجابة النطاق العام:

docker inspect --format '{{.State.Health.Status}}' shambus-coming-soon
curl -I https://shambus.com

للعودة الفورية إلى بوابة العميل الحالية:

docker compose \
--env-file .env.prod \
-f docker-compose.yml \
-f docker-compose.prod.yml \
--profile coming-soon \
stop coming-soon

إيقاف هذه الخدمة وحدها يزيل المسار ذي الأولوية الأعلى، فيعود Traefik تلقائياً إلى مسار customer المعتاد.

التحديثات

عند تحديث الإنتاج:

git pull
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod build customer dashboard admin docs
./infrastructure/scripts/run-migrations.sh
docker compose -f docker-compose.yml -f docker-compose.prod.yml --profile prod up -d

نشر تطبيق واحد دون قطع الخدمة

عندما يقتصر التغيير على تطبيق واحد، ابنِ صورته ثم استخدم ملف rollout بدلاً من إعادة إنشاء المكدس كله. مثال لوحة الشركات:

docker compose \
--env-file .env.prod \
-f docker-compose.yml \
-f docker-compose.prod.yml \
--profile prod \
build dashboard

docker rollout \
--env-file .env.prod \
-f docker-compose.yml \
-f docker-compose.prod.yml \
-f infrastructure/docker/docker-compose.rollout.yml \
--profile prod \
--timeout 180 \
--wait-after-healthy 5 \
dashboard

لا تستخدم --remove-orphans في rollout؛ توجد خدمات اختيارية مشروعة قد لا تكون ضمن profile الأمر الحالي. بعد نجاح الصحة، تحقق من المسارات المحمية ومن الإجراء الذي تغير، وليس من /health وحده.

حسابات QA المحمية

ضع بيانات حسابات فحص الإنتاج في ملف مؤقت بصلاحية 0600 خارج المستودع، واكتب القيم الحساسة بصيغة shell مقتبسة حتى لا تُفسَّر رموز مثل $ داخل كلمة المرور. لا تطبع الملف في السجل، ولا تحفظه في تقرير Playwright أو artifact. يجب أن تنتمي كل عمليات الإنشاء التجريبية إلى شركة QA داخلية معروفة، وأن تُلغى صلاحية حسابات الفريق المنشأة بعد الفحص مع إبقاء سجلها التشغيلي.

تتضمن مجموعة Playwright الإنتاجية المحمية 21 رحلة لمسارات الحجز والأدوار والموبايل وأداء صفحة الرحلات ودورة العرض المخصص، إضافة إلى ستة فحوص عامة للعرض 390×844. تشمل عقود الضغط المحدود 12 طلب بحث متزامناً، وخمس جلسات تتنافس على مقعد واحد، وستة حجوزات شركة تتنافس على مقعد واحد. اختبار العرض ينشئ tenant مؤقتاً ثم يلغيه ويحصد بياناته وينفذ تنظيفاً احتياطياً في teardown؛ لا تعطل هذا التنظيف لتسهيل التصحيح.

في فحص مركز بتاريخ 2026-08-10 نجحت عشرة عقود إنتاج خلال 15.2 ثانية، مع p95 قدره 298ms للبحث و242ms للقفل و318ms للحجز. هذه عينة محدودة وليست benchmark للسعة؛ لا ترفع التزامن أو عدد الطلبات في الإنتاج من دون نافذة تغيير ومراقبة مستقلة.

هذه المجموعة تحقق العقود المذكورة في tests/e2e/COVERAGE.md فقط. لا تستخدم العدد كادعاء جاهزية شاملة، ولا تعتبر نجاح first frame في Flutter بديلاً عن فحص جهاز أصلي والأذونات والعمل دون اتصال.

عند تشغيل Flutter Web محلياً، استخدم profile-mode لبوابة المتصفح وانتظر حدث flutter-first-frame بدلاً من اعتبار shell HTML نجاحاً. افحص أيضاً preflight من أصلي 8180 و8181: عقد CORS المركزي يجب أن يسمح بـ X-ShamBus-Client وIdempotency-Key. هذا فحص محلي مكمل ولا يساوي تحقق نطاقات الإنتاج أو بناء release.

تحديث مخطط PostgREST

أي ترحيل يضيف RPC أو يغير توقيع دالة يختتم بالتنبيه التالي:

NOTIFY pgrst, 'reload schema';

إذا أعاد API الخطأ PGRST202 مباشرة بعد ترحيل صحيح، أرسل التنبيه مرة واحدة ثم أعد اختبار العقد. لا تعالج المشكلة بإعادة بناء تطبيق الويب أو بتغيير توقيع RPC عشوائياً.

ملاحظات تشغيلية

  • ابدأ دائماً بالبنية الأساسية ثم الترحيلات ثم التطبيقات.
  • لا تضف ترحيلاً جديداً برقم مستخدم مسبقاً.
  • لا تستخدم القيم الافتراضية للتطوير في .env.prod.
  • لا تعتمد على bind mounts أو منافذ localhost في الإنتاج؛ ملف docker-compose.prod.yml هو من يفرض هذا الفصل.
  • إذا فشل السكربت أثناء الانتظار على خدمة ما، راجع السجلات التي يطبعها مباشرة قبل إعادة المحاولة.
  • إذا تم فصل قاعدة البيانات إلى VPS مستقل لاحقاً، أبقِ نفس مسار الترحيلات ولا تغيّر ترتيب الملفات.