اسأل عشرة مطورين «كيف تنشر تطبيقك؟» وسوف تسمع عشر إجابات مختلفة: الأول يفتح FileZilla ويسحب الملفات إلى السيرفر، والثاني يدخل بـ SSH وينفذ git pull، والثالث عنده سكربت اسمه deploy.sh كتبه زميل غادر الشركة قبل سنتين ولا يجرؤ أحد على تعديله، والرابع يدفع إلى Git فيبني خط CI ال Docker Image ويرفعها، والخامس لا ينشر أصلاً وإنما يدمج طلب دمج Pull Request في مستودع إعداد ويترك Argo CD يكمل الباقي. وكل واحد من هؤلاء يظن أن طريقته هي الطبيعية، والمشكلة أن أغلبهم لا يعرف ما الذي يفوته بالضبط، ولا ما هي الخطوة التالية المعقولة لفريقه، فإما أن يبقى على طريقة تسبب له توقفاً Downtime كل أسبوع، وإما أن يقفز إلى Kubernetes لأنه سمع أن الشركات الكبيرة تستخدمه.
وهذا المقال خريطة لكل طرق نشر التطبيقات Deployment التي مرت بها الصناعة، من الأقدم إلى الأحدث تقريباً، والهدف أن ترى طريقتك الحالية في مكانها من الخريطة، وتعرف ما الذي يتعطل فيها، ومتى تكون مقبولة فعلاً، وما الدرجة التالية التي تحل مشكلتك دون أن تجلب لك مشاكل لا تحتاجها. ولاحظ أن هذا المقال لا يعيد شرح أي أداة بالتفصيل، فلكل طريقة قسم قصير، وإذا كان لها شرح كامل في الموقع فسوف نضع الرابط إليه بدل أن نكرره. وسوف نناقش في هذا المقال ما يلي:
- الصورة الكاملة أولاً: ثلاث عشرة طريقة للنشر بالترتيب الذي ظهرت به تقريباً، والأسئلة الخمسة التي نسألها لكل طريقة.
- الطرق التي تنسخ فيها الأشياء إلى السيرفرات: رفع الملفات بـ FTP، والنسخ بـ rsync مع مجلد لكل إصدار، و
git pullعلى السيرفر، والحزم Artifacts، وأدوات إدارة الإعدادات Configuration Management، وال Golden Image، ومنصات PaaS. - الطرق التي تصف فيها ما يجب أن يعمل وتترك منصة تنفذه: ال Containers، والمنسقات Orchestrators، وخطوط CI/CD، واستراتيجيات الإطلاق Release Strategies، وGitOps، وServerless والحافة Edge.
- جدول يقارن كل الطرق في التوقف والتراجع Rollback والأسرار Secrets والانجراف Drift.
- تجربة عملية بمخرجات حقيقية لأربع طرق: مجلد لكل إصدار مع rsync ورابط يتبدل في لحظة واحدة، و
git pullومشكلته، وDocker Compose بوسم ال Image والتراجع عنه، والتبديل بين الأزرق والأخضر Blue-Green خلف Nginx. - سلم النضج Maturity Ladder وجدول تقييم سريع يبين أين تقف وما الخطوة التالية، ثم الأخطاء الشائعة وقائمة تحقق.
الصورة الكاملة: كيف تطور نشر التطبيقات
قبل أن ندخل في كل طريقة سوف نضع الخريطة كاملة أمامك، والصورة التالية تبين الطرق الثلاث عشرة بالترتيب الذي ظهرت به تقريباً، وهي مقسمة إلى صفين: في الصف الأول تنسخ أنت الأشياء إلى الأجهزة (ملفات، ثم مجلدات، ثم حزم، ثم Images كاملة للأجهزة)، وفي الصف الثاني تصف ما يجب أن يعمل وتترك منصة أو أداة تجعله يعمل:

ولاحظ أن الترتيب في الصورة تقريبي، فالطرق تداخلت في الزمن ولم تحل واحدة محل الأخرى في يوم محدد، وما زالت آلاف المواقع تنشر بـ FTP اليوم بجانب شركات تنشر بـ GitOps مئات المرات في اليوم. لذلك لا تقرأ الخريطة على أنها «القديم سيئ والجديد جيد»، وإنما على أنها سلسلة مشاكل وحلول: كل طريقة ظهرت لأن التي قبلها كانت تؤلم فريقاً ما في نقطة محددة. ولكي نقارن الطرق بنفس الميزان سوف نسأل كل طريقة خمسة أسئلة:
- التوقف Downtime: هل يرى المستخدم أخطاء أو موقعاً نصفه قديم ونصفه جديد أثناء النشر؟
- التراجع Rollback: إذا ظهر خطأ بعد النشر، كم دقيقة تحتاج لتعود إلى الإصدار السابق، وهل جربت ذلك من قبل؟
- الانجراف Drift: هل ما يعمل على السيرفر هو نفسه ما في Git، أم أن أحداً عدل شيئاً باليد ولا يعرفه غيره؟
- «يعمل على جهازي» Works on My Machine: هل البيئة التي اختبرت فيها هي نفس البيئة التي يعمل فيها الإنتاج Production؟
- الأسرار Secrets: أين تعيش كلمات مرور قاعدة البيانات والمفاتيح، ومن يستطيع قراءتها؟
ولكل طريقة سوف نذكر كيف تعمل، ومثالاً صغيراً حقيقياً، ولماذا انتشرت، وما الذي يتعطل فيها، ومتى ما زالت مقبولة اليوم، وما الخطوة التالية منها.
1. رفع الملفات بـ FTP والتعديل على السيرفر مباشرة
هذه هي الطريقة التي بدأ بها أغلبنا: استضافة مشتركة Shared Hosting فيها لوحة cPanel، وبرنامج مثل FileZilla يتصل بالسيرفر بالبروتوكول FTP، فتسحب مجلد الموقع من جهازك إلى المجلد public_html على السيرفر، وإذا أردت تعديلاً صغيراً فتحت الملف من مدير الملفات File Manager في cPanel وعدلته في المتصفح، أو دخلت بـ SSH وفتحته بالمحرر vim على السيرفر نفسه. والصورة التالية تبين ما يحدث في هذه الطريقة:

لماذا انتشرت: لأنها لا تحتاج إلى أي معرفة إضافية، فالاستضافة المشتركة رخيصة وتأتي معها لوحة تحكم وحساب FTP جاهز، والمطور يرى الملف ينتقل أمامه، ولمواقع PHP البسيطة لم يكن هناك أي خطوة بناء Build أصلاً، فالملف الذي تكتبه هو الملف الذي يعمل.
ما الذي يتعطل فيها: أول مشكلة أن الرفع يتم ملفاً بعد ملف، فإذا كان التحديث عشرين ملفاً يمر وقت يكون فيه الموقع نصفه قديم ونصفه جديد، ودالة جديدة في ملف وصل تستدعي ملفاً لم يصل بعد، فيرى الزائر خطأ 500. والمشكلة الثانية أن FTP العادي يرسل اسم المستخدم وكلمة المرور نصاً واضحاً Clear Text، وهذا موثق منذ 1999 في RFC 2577 الذي ينص على أن FTP يرسل كلمات المرور بالأمر PASS دون تشفير، لذلك إن كان لا بد من هذه الطريقة فاستخدم SFTP الذي يعمل فوق SSH. والمشكلة الثالثة أنه لا يوجد سجل للتغييرات، فتظهر على السيرفر ملفات مثل index.php.bak وindex-old2.php وهي «نظام إدارة النسخ» الوحيد، والتراجع يعني أن تتذكر أي الملفات تغيرت وترفعها من جديد. والرابعة أن التعديل المباشر على الإنتاج «مؤقتاً» لا يصل إلى جهاز المطور، فيمسحه الرفع التالي، أو يبقى السيرفر مختلفاً عن الكود الذي عند الجميع، وهذا هو الانجراف Drift في أبسط صوره. والخامسة أن إصدار PHP وإضافاته على السيرفر غير التي على جهازك، فيعمل الكود عندك ويتعطل هناك.
متى ما زالت مقبولة: موقع شخصي ثابت Static Site أو صفحة تعريفية لا يتغير فيها شيء إلا كل بضعة أشهر، على استضافة لا تعطيك غير SFTP، بشرط أن يكون الكود في Git على جهازك وأن ترفع بـ SFTP وليس FTP العادي.
الخطوة التالية: ضع الكود في Git أولاً كما شرحنا في إدارة النسخ وGit للمبتدئين، ثم توقف عن رفع الملفات واحداً واحداً، وارفع المجلد كاملاً إلى مكان جديد ثم بدل إليه في لحظة واحدة، وهذا هو موضوع القسم التالي.
2. النسخ بـ scp وrsync، وسكربت deploy.sh، ومجلد لكل إصدار
الخطوة الطبيعية بعد FTP أن تكتب سكربتاً، فبدل أن تسحب الملفات بالفأرة تنفذ scp أو rsync من جهازك أو من سيرفر البناء، والأداة rsync تنقل فقط الملفات التي تغيرت، فيصبح النشر أمراً واحداً يمكن تكراره. ولكن إذا نسخت مباشرة فوق المجلد الذي يعمل منه الموقع فأنت ما زلت في مشكلة «نصفه قديم ونصفه جديد»، لأن rsync أيضاً ينسخ ملفاً بعد ملف. والحل الذي انتشر مع أداة Capistrano في عالم Ruby ثم نسخته أدوات كثيرة هو أن يكون لكل إصدار مجلد مستقل، ورابط رمزي Symlink اسمه current يشير إلى الإصدار الذي يعمل، والصورة التالية تبين ذلك:

وهذا الشكل موثق في صفحة بنية المجلدات في Capistrano: مجلد releases فيه مجلد لكل نشر باسم التاريخ والوقت، ومجلد shared للملفات التي تبقى بين الإصدارات مثل الملفات المرفوعة وملف الإعداد، والرابط current الذي «يحدث في نهاية النشر الناجح، وإذا فشلت أي خطوة يبقى مشيراً إلى الإصدار القديم». وخادم الويب لا يعرف شيئاً عن الإصدارات، فهو يقرأ دائماً من /srv/app/current/public، والتراجع يعني أن تعيد الرابط إلى المجلد السابق الموجود أصلاً على القرص، وهذا يستغرق جزءاً من الثانية.
وقد يتساءل البعض: لماذا لا نغير الرابط بالأمر ln -sfn مباشرة؟ والإجابة أن ذلك يعتمد على أي نسخة من ln عندك، فبعض النسخ تحذف الرابط القديم ثم تنشئ الجديد، وبين الأمرين لحظة لا يوجد فيها current أصلاً، وسوف ترى ذلك بمخرجات حقيقية في التجربة العملية. والطريقة المضمونة هي التي يستخدمها Capistrano نفسه في كود المهمة deploy:symlink:release: أنشئ رابطاً مؤقتاً بالأمر ln -s، ثم انقله فوق current بالأمر mv، والسبب أن النقل داخل نفس نظام الملفات هو استدعاء النظام rename()، والذي ينص دليله على أنه إذا كان الاسم الجديد موجوداً فإنه «يستبدل بشكل ذري Atomic، فلا توجد لحظة يحاول فيها برنامج آخر الوصول إليه فلا يجده». ولهذا تسمى هذه الطريقة النشر الذري Atomic Deploy.
لماذا انتشرت: لأنها تحل التوقف والتراجع بسكربت من عشرين سطراً دون أي منصة، وتعمل مع أي لغة، ولا تحتاج على السيرفر إلا إلى SSH.
ما الذي يتعطل فيها: السيرفر نفسه ما زال يدار باليد، فإصدار PHP أو Node.js ومكتبات النظام تختلف بين السيرفرات، وإذا كان التطبيق يحتاج إلى تشغيل composer install أو npm ci على السيرفر بعد النسخ فقد عادت مشكلة «يعمل على جهازي». والسكربت نفسه يعيش غالباً على جهاز مطور واحد، والمفتاح الذي يدخل به إلى السيرفر معه أيضاً، وإذا تغير مخطط قاعدة البيانات فإن التراجع بالرابط لا يرجع قاعدة البيانات، وهذا موضوع سوف نعود إليه في استراتيجيات الإطلاق.
متى ما زالت مقبولة: موقع ثابت أو تطبيق PHP أو تطبيق مبني مسبقاً على سيرفر واحد أو اثنين، ينشر من خط CI وليس من جهاز مطور، وهي طريقة محترمة جداً لهذا الحجم، وكثير من مواقع الويب الكبيرة بقيت عليها سنوات.
الخطوة التالية: انقل تنفيذ السكربت إلى خط CI كما في خطوة النشر في Gitea Actions، وابن التطبيق مرة واحدة هناك بدل أن تبنيه على السيرفر.
3. الأمر git pull على السيرفر
عندما يصبح الكود في Git تخطر على البال فكرة مغرية: لماذا ننسخ الملفات أصلاً؟ ندخل إلى السيرفر بـ SSH، ونستنسخ المستودع مرة واحدة في مجلد الموقع، وكل نشر بعد ذلك هو cd /var/www/site && git pull، وأحياناً يضاف خطاف Webhook يجعل السيرفر ينفذ الأمر وحده بعد كل دفع Push.
لماذا انتشرت: لأنها لا تحتاج إلى أي سكربت، وتعطي إحساساً بأن السيرفر «متزامن مع Git»، وتنقل فقط ما تغير.
ما الذي يتعطل فيها: هذه الطريقة تجعل مجلد العمل Working Tree في Git هو الموقع نفسه، وهنا تبدأ المشاكل. فإذا عدل أحد ملفاً على السيرفر مباشرة، فإن git pull التالي يرفض أن يكمل، وسوف ترى ذلك حرفياً في التجربة العملية، والحل الذي ينفذه الناس تحت الضغط هو git reset --hard فيضيع التعديل دون أن يعرف أحد ماذا كان. والمشكلة الثانية أن المجلد .git موجود داخل مجلد الموقع، فإذا لم يمنعه خادم الويب صراحة يستطيع أي زائر أن يطلب /.git/config ويقرأ تاريخ الكود كاملاً، ومعه أي ملف .env دخل المستودع بالخطأ، وقد شرحنا في ما لا يجب أن يدخل المستودع أبداً واكتشاف الأسرار المسربة لماذا يبقى السر في التاريخ حتى بعد حذفه. والمشكلة الثالثة أن git pull أيضاً يكتب الملفات واحداً بعد الآخر، فلا يحل مشكلة «نصفه قديم ونصفه جديد»، والرابعة أن أي خطوة بناء (تثبيت الاعتماديات، تجميع ال CSS، ترحيل قاعدة البيانات) تحدث على سيرفر الإنتاج نفسه، فيحتاج السيرفر إلى أدوات البناء ومفاتيح المستودع، والخامسة أن التراجع هو git checkout لالتزام Commit قديم يدوياً، ولا أحد يعرف أي التزام كان يعمل قبل ساعة.
متى ما زالت مقبولة: سيرفر تجارب أو بيئة تطوير شخصية، أو موقع توثيق ثابت لا أسرار فيه، مع منع /.git في خادم الويب. أما على الإنتاج فلا تقم بذلك، والسبب أن الطريقة السابقة (rsync إلى مجلد جديد) أفضل منها في كل شيء تقريباً، ولا تكلف أكثر منها.
الخطوة التالية: لا تجعل مجلد Git هو الموقع، وانشر ناتج البناء وليس المستودع، بمجلد لكل إصدار كما في القسم السابق، أو بحزمة أو Docker Image كما في الأقسام التالية.
4. حزمة تبنى مرة واحدة: JAR وWAR وWeb Deploy وحزم deb وrpm
في عالم Java و.NET لم يكن نسخ الكود ممكناً أصلاً، لأن الكود يترجم Compile قبل أن يعمل، لذلك ظهرت مبكراً فكرة الحزمة Artifact: ملف واحد يخرج من خطوة البناء، له اسم ورقم إصدار، ويحمل كل ما يحتاجه التطبيق ليعمل. ففي Java يبنى التطبيق إلى ملف WAR ينشر على خادم Tomcat، إما بنسخه إلى مجلد webapps أو عبر تطبيق Tomcat Manager كما في دليل النشر في Tomcat، أو إلى ملف JAR يعمل وحده. وفي .NET على IIS توجد أداة Web Deploy (الأمر msdeploy.exe) التي تبني حزمة zip وتزامنها مع السيرفر، ومعها إعداد IIS وقاعدة البيانات عند الحاجة. وفي لينكس يمكن أن يكون التطبيق نفسه حزمة .deb أو .rpm تثبت بالأمر apt أو dnf، ولها سكربتات تعمل قبل التثبيت وبعده (preinst وpostinst) كما في دليل حزم Debian. والصورة التالية تبين الحزمة ومستودعها في الأعلى، وطريقتي توصيلها إلى السيرفرات في الأسفل، والتي سوف نشرحها في القسم التالي:

والفكرة المهمة هنا ليست شكل الملف وإنما القاعدة التي جاءت معه: ابن مرة واحدة Build Once، واحفظ الناتج في مستودع حزم Artifact Repository برقم إصدار لا يتغير، وانشر الملف نفسه على كل السيرفرات وكل البيئات، ولا تبن من جديد على السيرفر. ومستودع الحزم قد يكون Nexus أو Artifactory، أو ميزة الحزم في Gitea التي تستضيف حزم Maven وnpm وDebian وRPM وغيرها بجانب الكود.
لماذا انتشرت: لأن الإصدار أصبح شيئاً له اسم (1.4.2)، والتراجع أصبح «ثبت 1.4.1 مرة أخرى»، وما اختبره فريق ضمان الجودة QA هو حرفياً الملف نفسه الذي يصل إلى الإنتاج.
ما الذي يتعطل فيها: الحزمة لا تحمل بيئة التشغيل، فملف WAR يحتاج إلى إصدار Java وTomcat معين على السيرفر، وحزمة Web Deploy تحتاج إلى إصدار .NET معين على IIS، وهذه تثبت وتحدث باليد، فيعود الانجراف من باب آخر. والإعداد Configuration (عنوان قاعدة البيانات، كلمات المرور) يجب ألا يكون داخل الحزمة وإلا احتجت حزمة لكل بيئة، ويجب أن يكتب على كل سيرفر بطريقة ما، وهذا ما حلته أدوات القسم التالي.
متى ما زالت مقبولة: كثيراً، فتطبيقات .NET على IIS وتطبيقات Java على Tomcat في المؤسسات والجهات الحكومية ما زالت تنشر بهذه الطريقة، وهي جيدة إذا كان البناء في CI والإصدار مرقماً والإعداد خارج الحزمة.
الخطوة التالية: اكتب إعداد السيرفر نفسه كوداً بأداة إدارة إعدادات، أو انقل بيئة التشغيل كاملة داخل الحزمة بأن تجعل الحزمة Docker Image.
5. إدارة الإعدادات Configuration Management: Ansible وPuppet وChef وSalt
عندما يصبح عندك عشرة سيرفرات فإن الدخول إلى كل واحد منها لتثبيت الحزمة وكتابة ملف الإعداد وإعادة تشغيل الخدمة يصبح مصدراً دائماً للأخطاء، فواحد منها سوف ينسى دائماً. وأدوات إدارة الإعدادات تجعلك تكتب الحالة المطلوبة Desired State في ملفات: «هذه الحزمة بهذا الإصدار مثبتة، وهذا الملف بهذا المحتوى، وهذه الخدمة تعمل»، ثم تطبقها الأداة على كل السيرفرات، وإذا شغلتها مرة ثانية لا تغير شيئاً، وهذا هو التكرار الآمن Idempotency الذي شرحناه في البنية كشيفرة للمبتدئين، ومعه الفرق بين التجهيز Provisioning وإدارة الإعدادات في قسم مستقل.
وفي الصورة السابقة لاحظ أن هناك طريقتين لتوصيل الإعداد. الطريقة الأولى هي الدفع Push، وأشهر مثال لها Ansible، فمن جهاز تحكم Control Node تنفذ ansible-playbook فيتصل بكل سيرفر عبر SSH وينفذ المهام، ولا يحتاج إلى أي برنامج مثبت على السيرفرات، وتوثيق Ansible يذكر أن من مبادئ تصميمه أنه «بدون وكيل Agent-less». والطريقة الثانية هي السحب Pull، ومثالها Puppet، فعلى كل سيرفر وكيل Agent يرسل معلومات السيرفر إلى السيرفر الرئيسي ويطلب منه الإعداد المحسوب له Catalog ثم يطبقه، كما في شرح بنية Puppet، وChef يعمل بفكرة قريبة، إذ تسأل العقد Nodes سيرفر Chef عن الإعداد الذي يخصها. وميزة السحب أن الوكيل يعيد تطبيق الإعداد دورياً، فإذا عدل أحد ملفاً باليد أعاده الوكيل إلى ما هو مكتوب، وهذا يقلل الانجراف Drift الذي شرحناه في قسم الانحراف.
والمثال الصغير التالي يبين شكل مهمة نشر في Ansible:
# deploy.yml: install a fixed version of the app package on every web server
- hosts: web
become: true
tasks:
- name: Install shop 1.4.2
ansible.builtin.apt:
deb: "https://packages.example.com/shop_1.4.2_amd64.deb"
- name: Write the config from a template
ansible.builtin.template:
src: shop.env.j2
dest: /etc/shop/shop.env
mode: "0600"
notify: restart shop
handlers:
- name: restart shop
ansible.builtin.service:
name: shop
state: restartedلماذا انتشرت: لأنها حولت إعداد السيرفرات من وثيقة Word فيها خطوات إلى كود يراجع ويخزن في Git ويطبق على مئة سيرفر بنفس الطريقة.
ما الذي يتعطل فيها: السيرفر ما زال يتغير مع الزمن Mutable، فكل تشغيل يضيف أو يعدل فوق ما سبق، وما لا تذكره في الملفات لا تراقبه الأداة، فحزمة ثبتها أحد باليد قبل سنة تبقى. والنشر بإعادة التشغيل Restart على كل السيرفرات في نفس اللحظة يسبب توقفاً، إلا إذا رتبت التنفيذ على دفعات خلف موزع الحمل Load Balancer. والأسرار تحتاج إلى مكان آمن مثل Ansible Vault حتى لا تدخل المستودع نصاً واضحاً.
متى ما زالت مقبولة: إعداد السيرفرات نفسها (المستخدمين، جدار الحماية، Docker، المراقبة) مكانه الطبيعي في هذه الأدوات حتى اليوم، حتى لو كان التطبيق نفسه ينشر ك Docker Image.
الخطوة التالية: بدل أن تعدل السيرفر الذي يعمل، ابن Image جديدة للجهاز كله واستبدل السيرفر، أو انقل التطبيق إلى Container.
6. ال Golden Image والسيرفرات غير القابلة للتعديل Immutable
الفكرة هنا أن تتوقف عن إصلاح السيرفرات تماماً، فبدل أن تدخل إلى سيرفر يعمل وتحدثه، تبني Image كاملة للجهاز الافتراضي VM فيها نظام التشغيل والحزم والتطبيق بإصدار محدد، وتسمى Golden Image، ثم تشغل سيرفرات جديدة منها وتحذف القديمة. وفي AWS تسمى هذه ال Image باسم AMI، والأداة الأشهر لبنائها من ملف نصي هي Packer. والصورة التالية تقارن بين السيرفر الذي يعدل مع الزمن والسيرفر الذي يستبدل:

وهذه هي البنية غير القابلة للتعديل Immutable Infrastructure التي شرحنا فكرتها في البنية كشيفرة للمبتدئين. ويجدر بك أن تعرف أن Packer لم يعد مفتوح المصدر بالمعنى الدقيق، فملف ترخيصه اليوم هو Business Source License للإصدار 1.10.0 وما بعده، ويسمح بالاستخدام في الإنتاج ما لم تقدمه خدمة تنافس النسخة المدفوعة، وقد شرحنا قصة هذا التغيير عند HashiCorp في Terraform أم OpenTofu.
لماذا انتشرت: لأنها قتلت الانجراف من جذوره، فلا يوجد سيرفر «خاص» لا يعرف أحد كيف بني، وكل سيرفرين من نفس ال Image متطابقان، والتراجع هو تشغيل سيرفرات من ال Image السابقة، وتناسب التوسع الآلي Auto Scaling في السحابة لأن السيرفر الجديد يبدأ جاهزاً.
ما الذي يتعطل فيها: بناء Image لجهاز كامل يأخذ دقائق طويلة، وتشغيل سيرفر جديد يأخذ دقائق أخرى، لذلك يصبح كل تعديل صغير في الكود دورة طويلة. والأسرار يجب ألا تبنى داخل ال Image، وإنما تقرأ عند التشغيل، والبيانات يجب أن تعيش خارج السيرفر (قاعدة بيانات مستقلة، تخزين كائنات Object Storage) لأن السيرفر نفسه يحذف.
متى ما زالت مقبولة: لتجهيز Image أساسية لسيرفراتك (نظام محدث ومؤمن وعليه Docker والمراقبة) يعاد بناؤها كل شهر، وفي البيئات التي تتوسع آلياً في السحابة.
الخطوة التالية: نفس الفكرة بحجم أصغر بكثير: Image للتطبيق وحده وليس للجهاز كله، وهذا هو ال Container.
7. منصة PaaS: git push وتتكفل المنصة بالباقي
في المقابل ظهرت فكرة تخفي كل ما سبق عن المطور: منصة تطبيقات PaaS مثل Heroku، تدفع إليها الكود بالأمر git push heroku main فتكتشف لغة التطبيق وتبنيه وتشغله وتعطيك رابطاً، وفي توثيق Heroku «استخدم الأمر نفسه كلما أردت نشر آخر نسخة ملتزم بها من الكود». والذي يبني التطبيق هو حزم البناء Buildpacks، وهي بحسب صفحتها «مجموعة سكربتات تحول الكود إلى شيء قابل للنشر بأقل إعداد»، والمنصة تختار الحزمة المناسبة وحدها حسب لغة المشروع.
ومن هذه المنصات خرجت قواعد تطبيق العوامل الاثني عشر The Twelve-Factor App التي ما زالت مرجعاً حتى اليوم، وأهمها في موضوعنا الفصل الصارم بين البناء Build والإصدار Release والتشغيل Run، وأن «لكل إصدار معرفاً فريداً، والإصدارات سجل يضاف إليه فقط ولا يعدل»، وأن الإعداد يأتي من متغيرات البيئة وليس من الكود. ولاحظ أن هذه هي نفس فكرة «مجلد لكل إصدار» التي رأيناها في القسم الثاني، ولكن بلغة أكثر صرامة.
وإذا أردت التجربة نفسها على سيرفرك فهناك منصات تستضيفها بنفسك Self-hosted PaaS، منها Dokku الذي يصف نفسه بأنه «بديل مفتوح المصدر لـ Heroku» وترخيصه MIT، وCoolify بترخيص Apache-2.0، وDokploy الذي يقع أغلب مستودعه تحت Apache-2.0 مع مجلدات لميزات المؤسسات بترخيص آخر، وقد قارنا بين Coolify وDokploy وPortainer بالتفصيل وشرحنا ترخيص كل منها في مقارنة Portainer وCoolify وDokploy، ومعها دليلا تثبيت Coolify وتثبيت Dokploy.
لماذا انتشرت: لأن المطور لا يحتاج إلى معرفة أي شيء عن السيرفرات، والتراجع زر واحد، والبيئات متشابهة لأن المنصة نفسها تبني كل شيء.
ما الذي يتعطل فيها: المنصة المستضافة تكلف أكثر كلما كبرت، وتربطك بطريقتها في العمل، وبعض التطبيقات لا تناسب قيودها (عمليات طويلة، ملفات على القرص، منافذ غير HTTP). والمنصة التي تستضيفها بنفسك تعطيك نفس السهولة، ولكنها برنامج آخر تحدثه وتنسخه احتياطياً وتؤمنه.
متى ما زالت مقبولة: دائماً تقريباً لفريق صغير أو شركة ناشئة، والمنصة التي تستضيفها بنفسك خيار ممتاز لفريق يريد النشر من Git وبيئات المعاينة دون أن يكتب خط CI كاملاً.
الخطوة التالية: عندما تحتاج إلى تحكم أكبر في طريقة البناء والتشغيل، فإن ما تبنيه هذه المنصات اليوم هو Docker Image في النهاية، فتعلم ال Container نفسه.
8. ال Containers: من docker run إلى Compose وال Registry
ال Container جمع أفضل ما في الطرق السابقة: هو حزمة تبنى مرة واحدة برقم إصدار مثل القسم الرابع، ويحمل معه بيئة التشغيل كاملة (اللغة والمكتبات وإعدادها) مثل ال Golden Image في القسم السادس، ولكن بحجم التطبيق وحده، فيبنى في ثوان أو دقائق قليلة ويبدأ في ثوان. والتطبيق يوصف في ملف Dockerfile، وينتج عنه Docker Image ترفع إلى سجل Registry، والسيرفر يسحبها ويشغلها، والصورة التالية تبين هذا المسار:

والطريقة مرت بثلاث مراحل. في البداية كان الناس ينفذون docker run بأوامر طويلة يحفظونها في ملف نصي، ثم جاء Docker Compose فأصبح وصف التطبيق وقاعدة بيانه وشبكته في ملف compose.yaml واحد يدخل Git، ثم أصبح النشر هو تغيير وسم Tag ال Image في هذا الملف ثم docker compose pull وdocker compose up -d. وتثبيت Docker نفسه شرحناه في تثبيت Docker على Ubuntu.
وهنا نقطة يخطئ فيها كثيرون: الوسم مثل shop:1.1 أو latest هو اسم يمكن أن ينقل إلى Image أخرى في أي لحظة، أما البصمة Digest مثل shop@sha256:9fa8… فهي هاش محتوى ال Image نفسه، وتوثيق docker pull يسميها صراحة «المعرف الثابت Immutable Identifier». لذلك في الإنتاج انشر بالبصمة أو على الأقل بوسم لا يعاد استخدامه أبداً، وقد شرحنا ما يحدث عندما ترفع Image أخرى بنفس الوسم، وكيف توقع ال Images وتتحقق منها، في أمن سلسلة التوريد البرمجية.
وإذا أعجبتك فكرة Capistrano في القسم الثاني ولكن مع ال Containers، فهناك أداة Kamal بترخيص MIT، وهي تنشر Docker Images على سيرفرات عادية عبر SSH، ويذكر موقعها أنها تقدم «نشراً دون توقف Zero-downtime وإعادة تشغيل متدرجة».
لماذا انتشرت: لأنها أنهت مشكلة «يعمل على جهازي» تقريباً، فال Image التي اختبرتها هي البايتات نفسها التي تعمل في الإنتاج بنفس إصدار اللغة والمكتبات، والسيرفر لا يحتاج إلا إلى Docker، والتراجع هو الوسم السابق.
ما الذي يتعطل فيها: الأمر docker compose up -d يوقف ال Container القديم ثم يشغل الجديد، فبينهما ثوان لا يرد فيها التطبيق، وسوف تراها بالأرقام في التجربة العملية. والأسرار تتسرب إلى ال Image إذا نسختها داخلها أثناء البناء، وقد شرحنا ذلك في طبقات ال Docker Image. والتطبيق داخل Container خلف Reverse Proxy يحتاج إلى إعداد صحيح لعنوان الزائر و HTTPS، وهذا موضوع تطبيقك خلف Reverse Proxy. وعلى سيرفر واحد إذا توقف السيرفر توقف كل شيء.
متى ما زالت مقبولة: Compose على سيرفر واحد أو بضعة سيرفرات يكفي أغلب الفرق الصغيرة والمتوسطة، وهذا ما ناقشناه بالتفصيل في متى يكفيك Compose.
الخطوة التالية: ابن ال Image في خط CI وليس على جهازك، وأضف فحص الصحة Health Check وطريقة تبديل بلا توقف (القسم الحادي عشر)، وانتقل إلى منسق Orchestrator فقط عندما تحتاج عدة سيرفرات تعمل كسيرفر واحد.
9. المنسقات Orchestrators: Kubernetes وSwarm وNomad
عندما يصبح التطبيق عدة نسخ على عدة سيرفرات، تحتاج إلى من يقرر أين تعمل كل نسخة، ويعيد تشغيل ما يتوقف، ويستبدل النسخ القديمة بالجديدة دون توقف، وهذا هو عمل المنسق Orchestrator. وفي Kubernetes يكون النشر بكائن Deployment تكتب فيه الحالة المطلوبة (هذه ال Image بثلاث نسخ)، ثم ينفذ kubectl apply فيقوم Kubernetes بالتحديث التدريجي Rolling Update، وبحسب توثيق Deployment فإن القيمة الافتراضية تسمح بأن يكون 25% من النسخ زيادة أثناء التحديث (maxSurge) و25% غير متاحة (maxUnavailable)، فيرتفع عدد النسخ الجديدة وينخفض عدد القديمة على دفعات. وقد شرحنا ال Deployment وال ReplicaSet في مصطلحات Kubernetes، والتحديث التدريجي والتراجع في هل تحتاج Kubernetes فعلاً؟.
وهناك حلول أخف: Docker Swarm المبني داخل Docker، وفيه تحديث متدرج للخدمات بأمر واحد، وNomad من HashiCorp الذي يشغل ال Containers وغيرها، ولاحظ أن ترخيصه أصبح أيضاً Business Source License. والمقارنة بين هذه الخيارات وK3s على سيرفر واحد موجودة في حلول وسط بين Compose وKubernetes.
لماذا انتشرت: لأنها جعلت النشر دون توقف والإصلاح الذاتي Self-healing والتوسع سلوكاً افتراضياً بدل أن تكتبه بنفسك في سكربتات.
ما الذي يتعطل فيها: التعقيد، فالعنقود Cluster نفسه نظام تحتاج إلى تشغيله وتحديثه ومراقبته، والتحديث التدريجي يعني أن الإصدار القديم والجديد يخدمان المستخدمين في نفس الوقت، فيجب أن يتحمل الكود وقاعدة البيانات ذلك. وفحوص الجاهزية Readiness Probes إذا كانت خاطئة فإن Kubernetes يرسل الطلبات إلى نسخة لم تجهز بعد، أو يكمل تحديثاً فاشلاً.
متى تكون مناسبة: عندما يكون عندك أكثر من سيرفر فعلاً، وفريق يستطيع تشغيل العنقود، وعدة خدمات تنشر كل يوم. أما سيرفر واحد وتطبيق واحد فلا يحتاجان إلى ذلك.
الخطوة التالية: بعد أن يصبح النشر «تطبيق ملف YAML»، فالسؤال يصبح: من يطبق هذا الملف، ومن يوافق عليه؟ وهذا هو موضوع خطوط CI/CD وGitOps.
10. خط CI/CD: ابن مرة واحدة وانشر إلى عدة بيئات
كل الطرق السابقة يمكن أن تنفذ من جهاز مطور، وهذه هي المشكلة التي يحلها خط التكامل والتسليم المستمر CI/CD: كل دفع إلى Git يشغل خطاً آلياً Pipeline يبني الإصدار مرة واحدة، ويشغل الاختبارات والفحوص، ويحفظ الناتج (حزمة أو Docker Image)، ثم ينشره إلى بيئة staging، ثم إلى الإنتاج بعد موافقة Approval. وبهذا لا ينشر أحد من جهازه، ولا يصل إلى الإنتاج شيء لم يمر بالاختبارات، ولكل نشر سجل يبين من نشر ماذا ومتى.
وهذا الموضوع موزع في الموقع على عدة مقالات، فلا نكرره هنا: تشغيل الخط نفسه على سيرفرك مع Gitea Actions، وقاعدة «ابن مرة واحدة وانقل الشيء نفسه» بين البيئات في البيئات وGitOps للفرق، وأين يعمل كل اختبار في الخط في أنواع الاختبارات، وأين يعمل كل فحص أمني في DevSecOps للمبتدئين، واختبار الأحمال داخل الخط في اختبار الأحمال والضغط، وطريقة العمل على الفرع الرئيسي التي تجعل الدمج اليومي آمناً في التطوير على الفرع الرئيسي Trunk-Based. وإذا أردت مثالاً حقيقياً لخط نشر سريع ومتواضع في نفس الوقت، فقد عرضنا طريق Stack Overflow من commit إلى الإنتاج في دقائق، كما وصفه فريقهم في 2016، في قسم النشر من قصة Stack Overflow.
لماذا انتشرت: لأنها جعلت النشر حدثاً عادياً يتكرر عدة مرات في اليوم بدل حدث مخيف مرة في الشهر، والنشر الصغير المتكرر أسهل في الفهم والتراجع.
ما الذي يتعطل فيها: الخط نفسه يصبح أخطر مكان في الشركة، لأنه يملك مفاتيح الإنتاج (مفتاح SSH، أو ملف kubeconfig، أو توكن السحابة) ويشغل كوداً من كل طلب دمج، وقد شرحنا حوادث حقيقية لخطوط CI مخترقة في أمن سلسلة التوريد. والخط البطيء أو المتقلب Flaky يدفع الناس إلى تجاوزه «هذه المرة فقط».
متى تكون مناسبة: دائماً، فلا يوجد حجم فريق لا يستفيد من خط بسيط يبني ويختبر وينشر، حتى لو كانت خطوة النشر فيه هي سكربت rsync من القسم الثاني.
الخطوة التالية: اجعل طريقة استبدال الإصدار لا توقف الخدمة، ثم انقل مفاتيح الإنتاج من الخط إلى داخل البيئة نفسها، وهذا هو GitOps.
11. استراتيجيات الإطلاق Release Strategies: كيف تستبدل الإصدار دون أن يلاحظ المستخدم
إلى الآن تكلمنا عن كيف يصل الإصدار إلى السيرفر، أما هنا فالسؤال مختلف: عندما يكون الإصدار الجديد جاهزاً، كيف تنقل المستخدمين إليه؟ والصورة التالية تبين الطرق الأربع الأساسية:

وفي الصورة أعلاه لاحظ التالي:
- إعادة الإنشاء Recreate: أوقف القديم ثم شغل الجديد، وهذا ما يفعله
docker compose up -dمع Container واحد، وما تفعله الاستراتيجيةRecreateفي Kubernetes، وهي الأبسط والوحيدة التي تضمن ألا يعمل إصداران معاً، ولكن ثمنها ثوان من التوقف أو أكثر. - التحديث التدريجي Rolling: استبدل النسخ واحدة بعد واحدة، وهو الافتراضي في Kubernetes وSwarm، ولا يوقف الخدمة إذا كانت فحوص الصحة صحيحة، ولكن الإصدارين يخدمان المستخدمين معاً لدقائق، والتراجع تحديث تدريجي آخر بالاتجاه المعاكس.
- الأزرق والأخضر Blue-Green: بيئتان كاملتان متطابقتان، واحدة تعمل والأخرى تستقبل الإصدار الجديد وتختبر، ثم يحول الموجه Router كل الطلبات مرة واحدة، ومقال Martin Fowler يذكر أنها تعطيك «طريقة سريعة للتراجع، فإذا حدث خطأ تعيد الموجه إلى البيئة الزرقاء». وثمنها أنك تحتاج إلى ضعف الموارد أثناء النشر، وسوف تراها تعمل خلف Nginx في التجربة العملية.
- الكناري Canary: أرسل نسبة صغيرة من المستخدمين إلى الإصدار الجديد وراقب الأخطاء وزمن الرد، ثم زد النسبة تدريجياً أو ارجع إلى صفر، وهي بحسب تعريفها طريقة «لتقليل خطر إدخال إصدار جديد بطرحه ببطء على مجموعة صغيرة من المستخدمين قبل الجميع». وتحتاج إلى موجه يستطيع تقسيم الطلبات بالنسب، وإلى مقاييس Metrics تعرف منها أن الإصدار سيئ.
وفوق هذه الطرق الأربع توجد ثلاث أدوات تكملها. الأولى ال Feature Flags التي تفصل النشر Deploy عن الإطلاق Release، فيصل الكود إلى الإنتاج مطفأ ولا يظهر للمستخدمين حتى يفعل ال Flag، وقد شرحناها في البيئات وGitOps والتطوير على الفرع الرئيسي، ولها مقال مستقل مخطط في مسار التعلم. والثانية الإطلاق المظلم Dark Launch، وهو كما يعرفه Fowler أن تستدعي السلوك الجديد في الخلفية من طلبات المستخدمين الحاليين دون أن يروا نتيجته، لتقيس أثره على الحمل قبل أن تعلنه. والثالثة ترحيل قاعدة البيانات Database Migration، وهو الشيء الذي لا يرجع مع الرابط ولا مع الوسم، لذلك يكتب بنمط التوسيع ثم التقليص Expand and Contract الذي يسميه Fowler التغيير المتوازي Parallel Change: أضف العمود الجديد أولاً بحيث يعمل معه الكود القديم والجديد، ثم انقل البيانات، ثم احذف القديم في نشر لاحق. وقد شرحنا هذا النمط في تصميم قواعد البيانات، وفي ترحيل قاعدة البيانات بين البيئات، وعلى عدة مستأجرين في تطبيق SaaS متعدد المستأجرين.
وفي البيئات وGitOps ذكرنا قصة Knight Capital في 2012، حين بقي كود قديم على سيرفر واحد من ثمانية لم يصله النشر، وهي نفس المشكلة من الجهة الأخرى: نشر يدوي لا يتحقق أحد من أنه وصل إلى كل مكان.
متى تختار أيها: Recreate مقبول لأداة داخلية يتحمل مستخدموها دقيقة توقف، والتحديث التدريجي هو الافتراضي المعقول مع أي منسق، والأزرق والأخضر مناسب على سيرفر واحد أو اثنين لأنه لا يحتاج إلا إلى Reverse Proxy، والكناري يحتاج إلى عدد زوار كاف لتكون النسبة الصغيرة ذات معنى، وإلى مراقبة جيدة.
12. GitOps والإطلاق التدريجي Progressive Delivery
في خط CI/CD التقليدي الخط هو الذي يدفع Push النشر إلى السيرفرات، فيحتاج إلى مفاتيحها. وفي GitOps ينقلب الاتجاه: الحالة المطلوبة لكل بيئة مكتوبة في مستودع Git، وأداة تعمل داخل البيئة نفسها تسحب Pull هذا المستودع وتجعل البيئة تطابقه، وإذا غير أحد شيئاً باليد تعيده. والمبادئ الأربعة كما كتبها مشروع OpenGitOps هي أن تكون الحالة تصريحية Declarative، ومحفوظة بإصدارات لا تتغير Versioned and Immutable، وتسحبها الأداة تلقائياً Pulled Automatically، وتقارن باستمرار وتصحح Continuously Reconciled. والصورة التالية تقارن بين الطريقتين:

والأداتان الأشهر هما Argo CD وFlux، وكلاهما لـ Kubernetes. وقد شرحنا GitOps عملياً بمستودع إعداد وترقية بين البيئات بطلب دمج، مع Kubernetes ومع Docker Compose دون Kubernetes، في البيئات ومعاينة الميزات وGitOps للفرق، وشرحنا من أين جاءت الفكرة في البنية كشيفرة.
والإطلاق التدريجي Progressive Delivery هو الخطوة التي تجمع GitOps مع الكناري: أداة تنقل الطلبات إلى الإصدار الجديد على مراحل وتقرأ المقاييس في كل مرحلة، فإذا ارتفعت الأخطاء أعادت كل شيء وحدها دون أن يستيقظ أحد. ومن هذه الأدوات Argo Rollouts الذي يضيف إلى Kubernetes «الأزرق والأخضر والكناري وتحليل الكناري والإطلاق التدريجي»، وFlagger الذي «يحول الطلبات تدريجياً إلى الإصدار الجديد مع قياس المقاييس وتشغيل اختبارات المطابقة»، وكلاهما بترخيص Apache-2.0.
لماذا انتشرت: لأن مستودع الإعداد أصبح هو سجل التدقيق Audit Trail، فكل تغيير على الإنتاج commit له كاتب ومراجع، والتراجع git revert، ولا يحتاج خط CI إلى مفاتيح العنقود.
ما الذي يتعطل فيها: مستودعان بدل مستودع واحد، وأدوات إضافية تحتاج إلى تشغيل، والأسرار لا يجوز أن تدخل مستودع الإعداد نصاً واضحاً فتحتاج إلى حل مثل Sealed Secrets أو مخزن أسرار خارجي. والإطلاق التدريجي لا قيمة له إذا لم تكن عندك مقاييس موثوقة يحكم بها.
متى تكون مناسبة: عندما يكون عندك عدة بيئات وعدة خدمات، وأكثر من شخص يحتاج إلى أن يعرف ما الذي يعمل في الإنتاج ومن غيره. ولاحظ أن الفكرة نفسها (البيئة ملف في Git والترقية طلب دمج) تعمل مع Compose ومهمة CI بسيطة، دون Argo CD.
13. Serverless والحافة Edge والمواقع الثابتة مع CDN
آخر محطة في الخريطة تحذف السيرفر من نظرك تماماً: ترفع دالة Function واحدة، والمنصة تشغلها عند كل طلب وتوقفها عندما لا يوجد طلبات، وتدفع حسب الاستخدام. وقد أعلنت Amazon خدمة AWS Lambda في نوفمبر 2014، وتبعتها منصات تشغل الكود على حافة الشبكة Edge قرب الزائر مثل Cloudflare Workers الذي شرحناه مع بدائله في خدمات Cloudflare. وإذا أردت الفكرة على سيرفرك فهناك Knative Serving فوق Kubernetes بترخيص Apache-2.0، ويدعم إيقاف التطبيق إلى صفر نسخ Scale to Zero، وOpenFaaS الذي ينص ملف ترخيصه على أن نسخة المجتمع Community Edition تحتاج إلى ترخيص للاستخدام التجاري.
وبجانبها طريقة قديمة عادت بقوة: الموقع الثابت Static Site، فإذا كان موقعك صفحات HTML وCSS وJavaScript تبنى مسبقاً، فالنشر هو رفع مجلد إلى تخزين وأمامه شبكة توصيل محتوى CDN، والتراجع هو رفع المجلد السابق. والنقطة التي تحتاج إلى انتباه هي ال Cache، فالملف القديم قد يبقى عند الزوار أياماً، والحل أن يتغير اسم الملف مع محتواه كما شرحنا في شبكة توصيل المحتوى CDN.
لماذا انتشرت: لا يوجد سيرفر تحدثه، والتوسع آلي، والتكلفة صفر تقريباً عندما لا يوجد استخدام.
ما الذي يتعطل فيها: البدء البارد Cold Start في بعض المنصات، وحدود على زمن التنفيذ والذاكرة، والارتباط الشديد بمنصة واحدة، وصعوبة تشغيل التطبيق كاملاً على جهازك للاختبار، والتكلفة التي تقفز مع الاستخدام الكبير.
متى تكون مناسبة: للمواقع الثابتة دائماً تقريباً، وللمهام الصغيرة المتقطعة (معالجة ملف مرفوع، خطاف Webhook، تحويل صورة)، وليس بالضرورة لتطبيق كامل يعمل طوال اليوم.
المقارنة في جدول واحد: التوقف والتراجع والأسرار والانجراف
الجدول التالي يجمع الأسئلة الخمسة لكل طريقة، والمقصود بها الطريقة كما تطبق غالباً، وليس أفضل تطبيق ممكن لها:
| الطريقة | التوقف Downtime أثناء النشر | التراجع Rollback | الأسرار Secrets | الانجراف Drift و«يعمل على جهازي» |
|---|---|---|---|---|
| FTP والتعديل على السيرفر | نعم، الموقع نصفه قديم أثناء الرفع | يدوي، برفع الملفات القديمة إن وجدت | في ملفات على السيرفر، وكلمة مرور FTP نص واضح | عال جداً |
| rsync ومجلد لكل إصدار | لا، التبديل برابط واحد | ثوان، بإعادة الرابط | في shared/ على السيرفر | متوسط، السيرفر نفسه يدار باليد |
git pull على السيرفر | نعم، أثناء كتابة الملفات | يدوي بـ git checkout | خطر دخولها المستودع، و/.git مكشوف | عال، والتعديل اليدوي يوقف النشر |
| حزمة WAR أو Web Deploy أو deb | حسب إعادة التشغيل | تثبيت الإصدار السابق | خارج الحزمة، في ملف إعداد | متوسط، بيئة التشغيل تثبت باليد |
| إدارة الإعدادات | حسب الترتيب على دفعات | تشغيل الإصدار السابق من الملفات | Ansible Vault أو مخزن أسرار | منخفض لما هو مكتوب فقط |
| Golden Image | لا، مع موزع حمل | تشغيل سيرفرات من ال Image السابقة | تقرأ عند التشغيل، لا تبنى داخلها | منخفض جداً |
| PaaS | لا غالباً | زر أو أمر واحد | متغيرات بيئة في المنصة | منخفض |
| Docker Compose على سيرفر | ثوان مع Container واحد | الوسم السابق ثم up -d | ملف .env أو Docker Secrets | منخفض للتطبيق، والسيرفر يدار باليد |
| Kubernetes وSwarm وNomad | لا، تحديث تدريجي | أمر واحد أو الإصدار السابق من الملف | Secret في العنقود أو مخزن خارجي | منخفض |
| خط CI/CD | حسب طريقة الاستبدال | إعادة تشغيل الخط على إصدار سابق | في الخط، ومعها مفاتيح الإنتاج | منخفض إذا منع النشر اليدوي |
| GitOps | حسب طريقة الاستبدال | git revert | مشفرة في المستودع أو مخزن خارجي | يصحح تلقائياً |
| Serverless والمواقع الثابتة | لا غالباً | الإصدار السابق في المنصة | في إعداد المنصة | منخفض، والاختبار المحلي أصعب |
تجربة عملية: أربع طرق نشر على Containers صغيرة
الكلام عن التوقف والتراجع يبقى نظرياً حتى ترى الأرقام، لذلك سوف نشغل أربع طرق من الخريطة على جهاز واحد: مجلد لكل إصدار مع rsync ورابط current، ثم git pull على السيرفر ومشكلته، ثم Docker Compose بوسم ال Image والتراجع عنه، ثم التبديل الأزرق والأخضر خلف Nginx. وفي كل تجربة سوف نرسل طلبات متتالية إلى الموقع أثناء النشر ونعد ما رجع منها، فنرى بأعيننا هل توقف الموقع أم لا. والتجربة نفذت على Docker Engine 29.8.0 وDocker Compose v5.5.1، وكل الأسماء تبدأ بـ lab-deploy-، وكل المنافذ على 127.0.0.1 فقط.
تجهيز «السيرفر» و«خط CI»
نحتاج إلى Container يمثل سيرفر الويب، فيه Nginx وSSH وrsync وGit، وآخر يمثل خط CI أو جهاز المطور الذي ينشر منه، والملف التالي هو host/Dockerfile للسيرفر:
FROM alpine:3.22
RUN apk add --no-cache nginx openssh-server rsync git coreutils strace curl \
&& sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config \
&& passwd -u root 2>/dev/null; mkdir -p /root/.ssh /srv/app/releases && chmod 700 /root/.ssh
COPY default.conf /etc/nginx/http.d/default.conf
CMD ssh-keygen -A >/dev/null && /usr/sbin/sshd && exec nginx -g 'daemon off;'وإعداد Nginx في host/default.conf فيه موقعان: الأول على المنفذ 80 يقرأ من الرابط current، والثاني على المنفذ 8080 يقرأ من مجلد Git مباشرة لتجربة git pull:
server {
listen 80;
root /srv/app/current/public;
location / { try_files $uri $uri/ =404; }
}
server {
listen 8080;
root /srv/gitapp;
}وci/Dockerfile لجهة النشر:
FROM alpine:3.22
RUN apk add --no-cache openssh-client rsync git curl coreutils
CMD ["sleep", "infinity"]الآن نبني ونشغل، ونضع مفتاح SSH الخاص بجهة النشر في السيرفر:
docker build -q -t lab-deploy-host:1 host && docker build -q -t lab-deploy-ci:1 ci
docker network create lab-deploy-net
docker run -d --name lab-deploy-web --network lab-deploy-net -p 127.0.0.1:21280:80 -p 127.0.0.1:21281:8080 lab-deploy-host:1
docker run -d --name lab-deploy-ci --network lab-deploy-net lab-deploy-ci:1
docker exec lab-deploy-ci sh -c 'ssh-keygen -q -t ed25519 -N "" -f /root/.ssh/id_ed25519 && cat /root/.ssh/id_ed25519.pub' \
| docker exec -i lab-deploy-web sh -c 'cat > /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys'
docker exec lab-deploy-ci ssh -o StrictHostKeyChecking=accept-new lab-deploy-web 'echo ok; cat /etc/alpine-release'ومخرج الأمر الأخير سوف يكون كما يلي:
Warning: Permanently added 'lab-deploy-web' (ED25519) to the list of known hosts.
ok
3.22.6وفي داخل lab-deploy-ci نضع سكربتاً صغيراً اسمه watch.sh يرسل عدداً من الطلبات المتتالية إلى رابط، ويطبع لكل طلب محتوى الصفحة ورمز الرد، ثم يعد النتائج، فإذا فشل طلب في الاتصال يظهر رمزه 000:
#!/bin/sh
# watch.sh <url> <count>: request the page repeatedly and count what came back
for i in $(seq 1 "$2"); do
curl -s -m 2 -w '|%{http_code}' "$1" | tr -d '\n' | sed 's/<[^>]*>//g'
echo
sleep 0.01
done | sort | uniq -cمجلد لكل إصدار مع rsync ورابط يتبدل في لحظة واحدة
هذا هو سكربت النشر deploy.sh في lab-deploy-ci، وهو كل ما تحتاجه طريقة القسم الثاني:
#!/bin/sh
# deploy.sh: copy the build into a new release folder, then switch the current symlink atomically
set -eu
HOST=lab-deploy-web
APP=/srv/app
REL=$(date -u +%Y%m%d-%H%M%S)
rsync -a --delete build/ "$HOST:$APP/releases/$REL/"
ssh "$HOST" "
ln -s $APP/releases/$REL $APP/current.tmp &&
mv -T $APP/current.tmp $APP/current &&
cd $APP/releases && ls -1 | sort -r | tail -n +4 | xargs -r rm -rf
"
echo "deployed $REL"في الإعداد أعلاه لاحظ التالي:
- الأمر
rsyncينسخ إلى مجلد جديد باسم التاريخ والوقت، فلا يلمس الإصدار الذي يعمل أبداً، وإذا انقطع الاتصال في منتصفه يبقى الموقع على الإصدار القديم. - التبديل سطران: رابط مؤقت
current.tmp، ثمmv -Tفوقcurrent، والخيار-Tيمنعmvمن أن يعاملcurrentكمجلد فينقل الرابط إلى داخله. - السطر الأخير يحتفظ بآخر ثلاثة إصدارات فقط ويحذف الأقدم، حتى لا يمتلئ القرص.
الآن ننشر الإصدار الأول، ثم الإصدار الثاني بنفس الطريقة:
mkdir -p build/public && echo "<h1>shop v1</h1>" > build/public/index.html
./deploy.sh && curl -s http://lab-deploy-web/والمخرج سوف يكون كما يلي:
deployed 20261007-072835
<h1>shop v1</h1>والآن الاختبار المهم: ننشر الإصدار الثالث بينما watch.sh يرسل 300 طلب إلى الموقع:
(./watch.sh http://lab-deploy-web/ 300 > /tmp/w.txt &) ; sleep 1
echo "<h1>shop v3</h1>" > build/public/index.html
./deploy.sh; sleep 6; cat /tmp/w.txtوالمخرج سوف يكون كما يلي:
deployed 20261007-072908
68 shop v2|200
232 shop v3|200كل الطلبات الثلاثمئة رجعت بالرمز 200، والمستخدم رأى إما الإصدار الثاني كاملاً أو الثالث كاملاً، ولم يفشل أي طلب. وعلى السيرفر يبدو الشكل كما يلي:
docker exec lab-deploy-web sh -c 'ls -l /srv/app; ls -1 /srv/app/releases'total 4
lrwxrwxrwx 1 root root 33 Oct 7 07:29 current -> /srv/app/releases/20261007-072908
drwxr-xr-x 1 root root 4096 Oct 7 07:29 releases
20261007-072835
20261007-072849
20261007-072908والتراجع سكربت أصغر، يقرأ إلى أين يشير الرابط ويعيده إلى المجلد الذي قبله:
#!/bin/sh
# rollback.sh: point current back to the release before it
set -eu
HOST=lab-deploy-web
APP=/srv/app
ssh "$HOST" "
cur=\$(basename \$(readlink $APP/current))
prev=\$(ls -1 $APP/releases | sort -r | grep -A1 -x \$cur | tail -n 1)
[ \"\$prev\" != \"\$cur\" ] || { echo 'no older release'; exit 1; }
ln -s $APP/releases/\$prev $APP/current.tmp && mv -T $APP/current.tmp $APP/current
echo \"rolled back \$cur -> \$prev\"
"./rollback.sh && curl -s http://lab-deploy-web/والمخرج سوف يكون كما يلي:
rolled back 20261007-072908 -> 20261007-072849
<h1>shop v2</h1>والآن نعود إلى سؤال ln -sfn الذي طرحناه في القسم الثاني، فنراقب استدعاءات النظام التي ينفذها الأمر بالأداة strace، مرة مع ln من GNU coreutils، ومرة مع ln من BusyBox الموجود في كثير من Images الصغيرة:
cd /srv/app
ln --version | head -1
strace -f -e trace=symlink,symlinkat,unlink,unlinkat,rename,renameat,renameat2 ln -sfn /srv/app/releases/20261007-072908 current
strace -e trace=symlink,symlinkat,unlink,unlinkat,rename,renameat,renameat2 busybox ln -sfn /srv/app/releases/20261007-072908 current 2>&1 | grep -v exitedوالمخرج سوف يكون كما يلي:
ln (GNU coreutils) 9.7
symlinkat("/srv/app/releases/20261007-072908", AT_FDCWD, "current") = -1 EEXIST (File exists)
symlinkat("/srv/app/releases/20261007-072908", AT_FDCWD, "CuP2jhta") = 0
renameat(AT_FDCWD, "CuP2jhta", AT_FDCWD, "current") = 0
+++ exited with 0 +++
unlink("current") = 0
symlink("/srv/app/releases/20261007-072908", "current") = 0لاحظ الفرق: GNU coreutils الحديث ينشئ رابطاً باسم عشوائي ثم ينقله فوق current بـ renameat، وهذا سلوك أضيف في الإصدار 8.27 سنة 2017 كما في سجل تغييرات coreutils الذي ينص على أنه «لم تعد هناك لحظة قصيرة لا يوجد فيها الملف». أما BusyBox فيحذف current بـ unlink ثم ينشئه من جديد، وبين السطرين لحظة يرد فيها Nginx بخطأ 404 لأي طلب. لذلك لا تعتمد على ln -sfn واكتب الخطوتين صراحة كما في السكربت، فيعمل التبديل الذري على أي سيرفر.
الأمر git pull على السيرفر ومجلد العمل المعدل
الآن نجرب طريقة القسم الثالث، فننشئ على السيرفر مستودعاً فارغاً Bare Repository يدفع إليه المطور، ومجلداً مستنسخاً منه يقرؤه Nginx على المنفذ 8080:
# on the server
git init -q --bare -b main /srv/repo.git
# on the developer side: the first version, with a .env file that should never be here
mkdir -p /root/shop && cd /root/shop && git init -q
echo "<h1>shop v1</h1>" > index.html
printf "DB_HOST=db\nDB_PASSWORD=change-me\n" > .env
git add . && git commit -qm "v1"
git remote add origin lab-deploy-web:/srv/repo.git && git push -q origin main
# on the server: the site is a clone of the repository
git clone -q /srv/repo.git /srv/gitappثم نطلب من جهة أخرى الصفحة، والملف .git/config، والملف .env:
curl -s http://lab-deploy-web:8080/
curl -s -o /dev/null -w "%{http_code} /.git/config\n" http://lab-deploy-web:8080/.git/config
curl -s http://lab-deploy-web:8080/.git/config
curl -s -w "%{http_code} /.env\n" http://lab-deploy-web:8080/.envوالمخرج سوف يكون كما يلي:
<h1>shop v1</h1>
200 /.git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
[remote "origin"]
url = /srv/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
DB_HOST=db
DB_PASSWORD=change-me
200 /.envهذا هو الخطر الأول: Nginx لا يعرف أن .git و.env ملفات خاصة، فيقدمها لأي زائر يطلبها، ومن .git يمكن استرجاع تاريخ الكود كاملاً. والخطر الثاني يظهر عندما يعدل أحد ملفاً على السيرفر «بسرعة»، ثم يدفع المطور إصداراً جديداً:
# on the server: a quick fix by hand
cd /srv/gitapp && sed -i "s/v1/v1 (hotfix)/" index.html && git status --short
# on the developer side: v2
echo "<h1>shop v2</h1>" > index.html && git commit -qam "v2" && git push -q origin main
# on the server: the deploy
git pull; echo "exit=$?"; git log --oneline -1; cat index.htmlوالمخرج على السيرفر سوف يكون كما يلي:
M index.html
From /srv/repo
c856500..770c3ba main -> origin/main
Updating c856500..770c3ba
error: Your local changes to the following files would be overwritten by merge:
index.html
Please commit your changes or stash them before you merge.
Aborting
exit=1
c856500 v1
<h1>shop v1 (hotfix)</h1>النشر فشل، والموقع بقي على الإصدار الأول مع تعديل لا يعرفه Git، وإذا كان الأمر يعمل من خطاف آلي فلن يلاحظ أحد أن النشر لم يحدث. والحل الذي ينفذه الناس عادة تحت الضغط هو:
git reset --hard origin/main && cat index.html && git status --short | wc -lHEAD is now at 770c3ba v2
<h1>shop v2</h1>
0ونجح النشر، ولكن الإصلاح اليدوي اختفى دون أثر، فإن كان يصلح مشكلة حقيقية فقد عادت المشكلة الآن، ولا يوجد في أي مكان ما يخبرك بذلك.
Docker Compose بوسم ال Image والتراجع عنه
في هذه التجربة نبني إصدارين من التطبيق كـ Docker Images ونرفعهما إلى Registry محلي، ثم ننشر بتغيير الوسم في ملف .env. ال Dockerfile للتطبيق:
FROM nginx:1.29-alpine
ARG VERSION
RUN echo "<h1>shop ${VERSION}</h1>" > /usr/share/nginx/html/index.htmldocker run -d --name lab-deploy-registry -p 127.0.0.1:21282:5000 registry:3
for v in 1.0 1.1; do
docker build -q --build-arg VERSION=$v -t 127.0.0.1:21282/shop:$v app && docker push -q 127.0.0.1:21282/shop:$v
done
docker image inspect 127.0.0.1:21282/shop:1.0 --format '{{index .RepoDigests 0}}'
docker image inspect 127.0.0.1:21282/shop:1.1 --format '{{index .RepoDigests 0}}'ومخرج أوامر الرفع والفحص سوف يكون كما يلي:
127.0.0.1:21282/shop:1.0
127.0.0.1:21282/shop:1.1
127.0.0.1:21282/shop@sha256:db7ab396aee6206340c4f27d27b804a1711cf63d20bcf72b3f2d28ba78f2ed69
127.0.0.1:21282/shop@sha256:9fa8f46c763a0fed0774f576b8072751be9819336cd1824cc1512a35301f82f5والسطران الأخيران هما البصمتان Digests، وهما ما يجب أن ينشر في الإنتاج إذا أردت أن تضمن البايتات نفسها. وملف compose.yaml على «السيرفر»:
name: lab-deploy-app
services:
web:
image: 127.0.0.1:21282/shop:${TAG:?set TAG}
container_name: lab-deploy-app
ports:
- "127.0.0.1:21284:80"
networks: [lab-deploy-net]
restart: unless-stopped
networks:
lab-deploy-net:
external: trueنبدأ بالإصدار 1.0 في ملف .env (السطر TAG=1.0) وdocker compose up -d، ثم نرسل 400 طلب من lab-deploy-ci وننشر 1.1 أثناءها:
docker exec -d lab-deploy-ci sh -c '/root/watch.sh http://lab-deploy-app/ 400 > /tmp/c.txt'
sleep 1; sed -i 's/TAG=1.0/TAG=1.1/' .env
docker compose pull -q; docker compose up -d
sleep 9; docker exec lab-deploy-ci cat /tmp/c.txtوالمخرج سوف يكون كما يلي:
Image 127.0.0.1:21282/shop:1.1 Pulling
Image 127.0.0.1:21282/shop:1.1 Pulled
Container lab-deploy-app Recreate
Container lab-deploy-app Recreated
Container lab-deploy-app Starting
Container lab-deploy-app Started
276 shop 1.0|200
93 shop 1.1|200
31 |000هنا فشل 31 طلباً من 400 في الاتصال أصلاً، وهي الطلبات التي وصلت بين إيقاف ال Container القديم وتشغيل الجديد، وهذه هي استراتيجية إعادة الإنشاء Recreate بالأرقام. والتراجع هو نفس الأمر بالوسم السابق:
docker exec -d lab-deploy-ci sh -c '/root/watch.sh http://lab-deploy-app/ 400 > /tmp/c.txt'
sleep 1; sed -i 's/TAG=1.1/TAG=1.0/' .env; docker compose up -d
sleep 9; docker exec lab-deploy-ci cat /tmp/c.txtوعد الطلبات هذه المرة كان كما يلي:
218 shop 1.0|200
146 shop 1.1|200
36 |000التراجع سريع وسهل لأن ال Image السابقة ما زالت في ال Registry وعلى السيرفر، ولكنه أيضاً أوقف الموقع لحظات، وهذا مقبول لأداة داخلية وغير مقبول لمتجر في وقت الذروة. ولاحظ أن عدد الطلبات الفاشلة يختلف من مرة إلى أخرى، فهو يعتمد على سرعة إيقاف ال Container وتشغيله على جهازك، والمهم أنه ليس صفراً.
التبديل الأزرق والأخضر Blue-Green خلف Nginx
لكي يصبح التوقف صفراً نشغل الإصدارين معاً، ونضع أمامهما Nginx ك Reverse Proxy يرسل الطلبات إلى واحد منهما فقط:
docker run -d --name lab-deploy-blue --network lab-deploy-net 127.0.0.1:21282/shop:1.0
docker run -d --name lab-deploy-green --network lab-deploy-net 127.0.0.1:21282/shop:1.1
docker run -d --name lab-deploy-lb --network lab-deploy-net -p 127.0.0.1:21283:80 nginx:1.29-alpineوإعداد Nginx في /etc/nginx/conf.d/default.conf داخل lab-deploy-lb يقرأ عنوان النسخة الحية من ملف صغير منفصل:
upstream app {
include /etc/nginx/active.conf;
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_set_header Host $host;
}
}والتبديل سكربت اسمه switch يكتب الملف ويختبر الإعداد ويطلب من Nginx إعادة قراءته، وإذا فشل الاختبار يعيد الملف كما كان:
#!/bin/sh
# switch <blue|green>: send all new requests to the chosen colour
set -eu
cp /etc/nginx/active.conf /etc/nginx/active.conf.bak
echo "server lab-deploy-$1:80;" > /etc/nginx/active.conf
if ! nginx -t -q; then
cp /etc/nginx/active.conf.bak /etc/nginx/active.conf
echo "config test failed, still live: $(cat /etc/nginx/active.conf)"
exit 1
fi
nginx -s reload
echo "live: $1"والسبب في أن هذا لا يوقف أي طلب أن الأمر nginx -s reload بحسب توثيق Nginx يجعل العملية الرئيسية تتحقق من الإعداد، ثم تشغل عمليات عمل Workers جديدة، وتطلب من القديمة أن تتوقف بهدوء بعد أن تنهي الطلبات التي معها. وقبل التبديل يختبر الفريق الأخضر مباشرة دون أن يراه المستخدمون:
docker exec lab-deploy-ci curl -s http://lab-deploy-green/<h1>shop 1.1</h1>الآن نرسل 600 طلب ونبدل إلى الأخضر في منتصفها، ثم نكرر ذلك للعودة إلى الأزرق، وهذا هو التراجع:
docker exec lab-deploy-ci /root/watch.sh http://lab-deploy-lb/ 600 > result.txt &
sleep 3; docker exec lab-deploy-lb switch green; wait; cat result.txt
docker exec lab-deploy-ci /root/watch.sh http://lab-deploy-lb/ 600 > result.txt &
sleep 3; docker exec lab-deploy-lb switch blue; wait; cat result.txtوالمخرج سوف يكون كما يلي:
2026/10/07 07:37:04 [notice] 232#232: signal process started
live: green
149 shop 1.0|200
451 shop 1.1|200
2026/10/07 07:37:22 [notice] 250#250: signal process started
live: blue
504 shop 1.0|200
96 shop 1.1|200كل الطلبات الألف والمئتين رجعت بالرمز 200 في الاتجاهين، مقارنة بـ 31 و36 طلباً فاشلاً مع Compose العادي. والثمن أن الإصدارين يعملان معاً، أي ضعف الذاكرة أثناء النشر، وأن قاعدة البيانات يجب أن تخدم الإصدارين معاً، وهذا سبب نمط التوسيع ثم التقليص الذي ذكرناه في استراتيجيات الإطلاق.
والآن اكسرها لتفهمها: في النسخة الأولى من السكربت كتبنا التبديل بسطر واحد كما يكتبه أغلب الناس:
#!/bin/sh
set -eu
echo "server lab-deploy-$1:80;" > /etc/nginx/active.conf
nginx -t -q && nginx -s reload
echo "live: $1"ثم أوقفنا lab-deploy-green وطلبنا التبديل إليه:
docker stop lab-deploy-green; docker exec lab-deploy-lb switch greenوالمخرج كان كما يلي:
2026/10/07 07:34:53 [emerg] 105#105: host not found in upstream "lab-deploy-green:80" in /etc/nginx/active.conf:1
nginx: [emerg] host not found in upstream "lab-deploy-green:80" in /etc/nginx/active.conf:1
nginx: configuration file /etc/nginx/nginx.conf test failed
live: greenNginx رفض الإعداد واستمر يخدم الأزرق، وهذا جيد، ولكن السكربت طبع live: green وخرج بنجاح، وبقي الملف active.conf يقول أخضر بينما الحي هو الأزرق، فأول إعادة تشغيل لـ Nginx بعد ذلك سوف تفشل. والسبب قاعدة في الشل يجهلها كثيرون: الخيار set -e بحسب معيار POSIX يتجاهل فشل «أي أمر في قائمة AND-OR غير الأمر الأخير»، أي أن فشل nginx -t في nginx -t -q && nginx -s reload لا يوقف السكربت. لذلك اكتب الفحص بـ if صريح كما في النسخة النهائية، وبها يكون مخرج الأمر docker exec lab-deploy-lb switch green; echo "exit=$?" في نفس الموقف كما يلي:
2026/10/07 07:35:16 [emerg] 136#136: host not found in upstream "lab-deploy-green:80" in /etc/nginx/active.conf:1
nginx: [emerg] host not found in upstream "lab-deploy-green:80" in /etc/nginx/active.conf:1
nginx: configuration file /etc/nginx/nginx.conf test failed
config test failed, still live: server lab-deploy-blue:80;
exit=1وهذا هو الدرس الذي يتكرر في كل سكربتات النشر: السكربت الذي يقول «نجح» وهو لم ينجح أخطر من السكربت الذي يفشل بصوت عال.
التنظيف
نحذف ما أنشأناه فقط بالأسماء، مع ال Volume المجهول الذي ينشئه ال Registry (لذلك الخيار -v):
docker compose -p lab-deploy-app down
docker rm -fv lab-deploy-web lab-deploy-ci lab-deploy-registry lab-deploy-blue lab-deploy-green lab-deploy-lb
docker network rm lab-deploy-net
docker rmi lab-deploy-host:1 lab-deploy-ci:1 127.0.0.1:21282/shop:1.0 127.0.0.1:21282/shop:1.1ولا تستخدم docker system prune أو docker volume prune على جهاز فيه مشاريع أخرى، والسبب أنها تحذف كل ما لا يعمل الآن، ومنه Volumes لمشاريع متوقفة مؤقتاً.
أين تقف أنت؟ سلم النضج وتقييم سريع
بعد أن رأيت الخريطة كاملة يأتي السؤال العملي: أين فريقك عليها، وما الخطوة التالية؟ والصورة التالية ترتب الطرق في سلم من سبع درجات، وكل درجة تحل مشكلة واضحة في التي تحتها:

ولكي تعرف درجتك أجب عن الأسئلة التالية بالترتيب، وأول سؤال تجيب عنه بـ «لا» هو درجتك الحالية، والعمود الأخير هو الخطوة التالية:
| السؤال | إذا كانت الإجابة «لا» فأنت في | الخطوة التالية |
|---|---|---|
| هل الكود كله في Git، ولا يعدل أحد ملفاً على السيرفر مباشرة؟ | الدرجة 0 | Git أولاً كما في إدارة النسخ وGit، ثم منع التعديل على الإنتاج. |
| هل النشر أمر واحد، ولا يرى المستخدم الموقع نصفه قديم؟ | الدرجة 0 | سكربت rsync مع مجلد لكل إصدار ورابط current كما في التجربة. |
| هل تعرف بالضبط أي إصدار يعمل الآن (رقم إصدار أو SHA لل commit)؟ | الدرجة 1 | اعرض الإصدار في التطبيق أو في رابط /version، ووسم كل نشر. |
| هل ما يصل إلى الإنتاج هو الملف نفسه الذي اختبر، دون بناء على السيرفر؟ | الدرجة 1 | حزمة أو Docker Image تبنى مرة واحدة، كما في ابن مرة واحدة. |
| هل يبني ويختبر وينشر خط آلي، ولا ينشر أحد من جهازه؟ | الدرجة 2 | خط CI بسيط كما في Gitea Actions، مع staging وموافقة قبل الإنتاج. |
| هل جربت التراجع في الشهر الأخير، وهل يأخذ أقل من خمس دقائق؟ | الدرجة 2 | اكتب خطوات التراجع وجربها على staging، واحتفظ بالإصدارات السابقة. |
| هل تستطيع إعادة بناء السيرفر من الصفر من ملفات في Git؟ | الدرجة 3 | Compose وإعداد السيرفر كملفات، مع البنية كشيفرة. |
| هل الأسرار خارج Git وخارج ال Images، وهل يفحص الخط الكود بحثاً عنها؟ | الدرجة 3 | متغيرات بيئة أو مخزن أسرار، مع فحص الأسرار. |
| هل يمر النشر دون أن يفشل أي طلب، وهل التغيير على الإنتاج commit يراجعه شخص ثان؟ | الدرجة 4 | Blue-Green أو تحديث تدريجي، ومستودع إعداد كما في GitOps للفرق. |
| هل تكتشف الإصدار السيئ قبل أن يصل إلى كل المستخدمين؟ | الدرجة 5 | كناري مع مقاييس وتراجع آلي، أو Feature Flags تطلق الميزة لنسبة صغيرة. |
وإذا أردت أن تقيس التحسن بالأرقام بدل الإحساس، فمقاييس DORA هي المرجع المعروف: مدة وصول التغيير Change Lead Time من commit إلى الإنتاج، وتكرار النشر Deployment Frequency، ونسبة النشرات التي تحتاج تدخلاً عاجلاً Change Fail Rate، ووقت التعافي من نشر فاشل Failed Deployment Recovery Time، ونسبة النشرات غير المخطط لها بسبب حادث Deployment Rework Rate. وهذه أرقام يمكنك حسابها من سجل خط CI وتذاكر الحوادث دون أي أداة إضافية.
أخطاء شائعة
- التعديل المباشر على الإنتاج «مؤقتاً»: يمسحه النشر التالي، أو يوقفه كما رأيت مع
git pull، أو يبقى سراً لا يعرفه إلا من نفذه، والإصلاح العاجل يمر بنفس الطريق بمراجعة أسرع. - البناء على سيرفر الإنتاج:
npm installأوcomposer installعلى السيرفر بعد كل نشر يعني أن الإنتاج يشغل شيئاً لم يختبره أحد، ويحتاج إلى أدوات البناء ومفاتيح المستودع. - الوسم
latestفي الإنتاج: لا تعرف أي إصدار يعمل، والتراجع إلى «latest السابق» مستحيل، فاستخدم رقم إصدار أو البصمة Digest. - التراجع الذي لم يجرب: الفريق يكتشف وقت المشكلة أن الإصدار السابق حذف من ال Registry، أو أن الترحيل حذف عموداً يحتاجه الكود القديم.
- ترحيل قاعدة البيانات الذي يكسر الإصدار السابق: تغيير اسم عمود أو حذفه في نفس النشر الذي يغير الكود، بدل التوسيع ثم التقليص على نشرين.
- الأسرار في المستودع أو داخل ال Image: ملف
.envفي Git أو منسوخ في ال Dockerfile، وقد رأيت في التجربة كيف يقدمه Nginx لأي زائر. - سكربتات نشر تخفي الفشل:
set -eمع&&، أو خطوة تطبع «تم» دون أن تتحقق، والأصح أن ينتهي كل نشر بطلب حقيقي يتأكد أن الإصدار الجديد يرد. - القفز إلى أداة لا يحتاجها الفريق: Kubernetes لتطبيق واحد على سيرفر واحد يضيف عملاً أكثر مما يوفر، والدرجة التالية في السلم دائماً أرخص من الدرجة التي بعدها.
قائمة تحقق قبل أن تعتمد طريقة نشر
- هل يبنى الإصدار مرة واحدة في مكان واحد، ويحمل رقماً أو SHA تستطيع أن تراه في الإنتاج؟
- هل يكتمل الإصدار الجديد قبل أن يراه المستخدم، ثم يتم التبديل بخطوة واحدة؟
- هل يمر كل نشر بالاختبارات وبيئة تشبه الإنتاج، ويحتاج الإنتاج إلى موافقة؟
- هل التراجع مكتوب ومجرب، والإصدارات السابقة محفوظة، والترحيلات متوافقة مع الإصدار السابق؟
- هل الأسرار خارج Git وخارج ال Images، ومن يملك مفاتيح الإنتاج معروف ومحدود؟
- هل يمنع خادم الويب
/.gitوالملفات المخفية، ولا يحتاج السيرفر إلى أدوات البناء؟ - هل ينتهي النشر بفحص حقيقي للتطبيق، ويفشل بصوت عال إذا لم يرد؟
- هل تستطيع إعادة بناء السيرفر وبيئته من ملفات في Git دون الرجوع إلى ذاكرة أحد؟
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- كل طرق النشر من FTP إلى GitOps هي سلسلة مشاكل وحلول: FTP ترك الموقع نصفه قديم، فجاء مجلد لكل إصدار ورابط يتبدل، ثم الحزمة التي تبنى مرة واحدة، ثم إدارة الإعدادات، ثم ال Images التي تستبدل السيرفر، ثم ال Containers والمنسقات، ثم الخطوط الآلية وGitOps.
- اسأل أي طريقة خمسة أسئلة: هل يتوقف الموقع، وكم يأخذ التراجع، وأين الأسرار، وهل ينجرف السيرفر عن Git، وهل البيئة التي اختبرت فيها هي بيئة الإنتاج.
- التبديل الذري برابط
currentوmv -Tلم يفشل فيه أي طلب من 300، وdocker compose up -dفشل فيه 31 طلباً من 400، والتبديل الأزرق والأخضر خلف Nginx لم يفشل فيه أي طلب من 1200. git pullعلى السيرفر يكشف.gitو.envللزوار، ويتوقف عند أول تعديل يدوي، فلا تستخدمه على الإنتاج.- الوسم اسم يتحرك والبصمة Digest هي البايتات نفسها، والترحيل يكتب بالتوسيع ثم التقليص حتى يبقى التراجع ممكناً.
- أطلق الإصدار لنسبة صغيرة أولاً كلما استطعت، فقصة CrowdStrike في 2024 تبين ما يحدث عندما يصل التحديث إلى الجميع في نفس اللحظة.
- اعرف درجتك في السلم واصعد درجة واحدة، والفريق الصغير على سيرفر واحد يكفيه في الغالب خط CI وDocker Image وCompose وتبديل بلا توقف وتراجع مجرب.
دليلمن الفرع إلى الإنتاج: البيئات وGitOps للفرقكيف يختبر فريقك كل ميزة وحدها ويعرضها على العميل دون أن تصل ميزة غير جاهزة إلى الإنتاج.
دليلGitea Actions: تشغيل CI/CD وبناء Docker Images على سيرفركاجعل سيرفرك يختبر الكود ويبني Docker Images تلقائياً مع كل push، دون أن يخرج كودك أو أسرارك إلى خدمة CI خارجية.
دليلهل تحتاج Kubernetes فعلاً؟ متى يكفي Docker Composeما الذي يحله Kubernetes وما الذي يكلفك، ومتى يكفيك Docker Compose على سيرفر واحد أو بضعة سيرفرات.
مقارنةأي أداة تدير بها تطبيقاتك على السيرفر؟متى تكفيك واجهة تدير بها ال Containers، ومتى تحتاج منصة تنشر تطبيقاتك من Git مباشرة على سيرفرك.
دليلمصطلحات البنية كشيفرة IaC التي تحتاجها قبل أي أداةما معنى أن تكتب بنيتك في ملفات داخل Git، ولماذا يفشل السكربت العادي عند تشغيله مرة ثانية، وما الذي تحفظه الأداة في ملف الحالة.
دليلأمن سلسلة التوريد البرمجية Software Supply Chain للمبتدئينكل ما بين كود غيرك وسيرفرك قد يحمل كود المهاجم، وهنا تعرف التهديدات الحقيقية بحوادث موثقة والدفاع المناسب لكل حلقة.سجل التحديثات
- أكتوبر 2026: كتابة المقال، وتنفيذ التجربة العملية على Docker Engine 29.8.0 وDocker Compose v5.5.1 وalpine 3.22.6 وrsync 3.5.0 وOpenSSH 10.0p2 وGit 2.49.1 وGNU coreutils 9.7 وBusyBox 1.37.0 وnginx 1.29.8 وRegistry 3.1.2.