نشر بيئة الإنتاج
الهدف
بيئة الإنتاج المعتمدة الآن مبنية على:
- 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 لا بوسوم عائمة:
| المكوّن | الإصدار |
|---|---|
| PostgreSQL | 15.14.1.116 |
| Kong | 3.9.3 |
| GoTrue | 2.189.0 |
| PostgREST | 14.12 |
| Realtime | 2.102.3 |
| Storage API | 1.60.4 |
| Postgres Meta | 0.96.6 |
| Supabase Studio | 2026.08.03-sha-022b374 |
| Authentik | 2026.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/Kongstudio.shambus.comلـ Supabase Studiografana.shambus.comللمراقبةerrors.shambus.comلـ GlitchTipntfy.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. أهم المفاتيح:
DOMAINACME_EMAILPOSTGRES_PASSWORDJWT_SECRETSECRET_KEY_BASE(مستقل، 64 محرفاً على الأقل)REALTIME_DB_ENC_KEY(16 محرفاً تماماً)PG_META_CRYPTO_KEY(32 محرفاً على الأقل)SUPABASE_ANON_KEYSUPABASE_SERVICE_ROLE_KEYTWILIO_MESSAGE_SERVICE_SIDTRAEFIK_DASHBOARD_AUTHGRAFANA_ADMIN_PASSWORDGLITCHTIP_SECRET_KEYGLITCHTIP_POSTGRES_PASSWORDMINIO_ROOT_PASSWORDGOOGLE_MAPS_API_KEY_WEB(اختياري، مقيّد بنطاقات الويب)GOOGLE_MAPS_API_KEY_ANDROID(اختياري، مقيّد بتطبيقات Android)GOOGLE_MAPS_API_KEY_IOS(اختياري، مقيّد بحزم iOS)SMTP_*CUSTOMER_GLITCHTIP_DSNDASHBOARD_GLITCHTIP_DSNADMIN_GLITCHTIP_DSNMOBILE_CUSTOMER_GLITCHTIP_DSNMOBILE_DRIVER_GLITCHTIP_DSNGOOGLE_OAUTH_WEB_CLIENT_ID(جمهور التحقق على الخادم وserver client للموبايل)GOOGLE_OAUTH_IOS_CLIENT_IDGOOGLE_OAUTH_IOS_REVERSED_CLIENT_IDTWILIO_*NEXT_PUBLIC_VAPID_PUBLIC_KEYVAPID_PRIVATE_KEYVAPID_SUBJECTDEMO_ACCESS_SECRETCRON_SECRETDEMO_HUB_URLDEMO_DASHBOARD_URLDEMO_DRIVER_URLWEB_API_URLPUBLIC_SITE_MODEMAINTENANCE_INTERVAL_SECONDSPGRST_DB_SCHEMAS=publicPGRST_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_IDGOOGLE_OAUTH_IOS_CLIENT_IDGOOGLE_OAUTH_IOS_REVERSED_CLIENT_IDWEB_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/healthdashboard /api/healthadmin /api/healthdocs /healthmobile-customer /healthmobile-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_*لحساب عميل داخلي يعتمد على OTPINTERNAL_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_URLSUPABASE_ANON_KEYWEB_API_URLNTFY_URLCERTIFICATE_PINSENVIRONMENT=productionGOOGLE_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/tcp443/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.comhttps://dashboard.shambus.comhttps://admin.shambus.comhttps://api.shambus.comhttps://studio.shambus.comhttps://grafana.shambus.comhttps://errors.shambus.comhttps://demo.shambus.com/demohttps://demo-dashboard.shambus.com/demo/accesshttps://demo-driver.shambus.comhttps://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 مستقل لاحقاً، أبقِ نفس مسار الترحيلات ولا تغيّر ترتيب الملفات.