كيفية تفعيل التحديث المباشر (Live-reload) في تطبيقات Docker باستخدام وحدات تخزين Docker (Volumes)

دقائق القراءة: 10
في هذا المقال، ستتعلم كيفية إعداد بيئة تطوير متكاملة ومزودة بميزة التحديث المباشر (live-reload)، مما سيمكنك من تحويل تطبيق قديم (legacy application) ليعمل بكفاءة ضمن بيئة Docker، مستفيدًا من وحدات تخزين Docker Volumes وخدمة docker-compose. يرى بعض المطورين أن Docker غير مناسب لبيئات التطوير، بحجة أنه يتطلب إعادة بناء الصورة (image) بالكامل مع كل تعديل جديد، مما يجعله بطيئًا وغير منتج. هدفنا في هذا المقال هو دحض هذه الفكرة من خلال إظهار كيف يمكن لتكوينات بسيطة أن تحقق فوائد جمة، مثل توفير بيئة موثوقة ومتطابقة بين التطوير والإنتاج. بنهاية هذا المقال، ستكون قد اكتسبت القدرة على:
  • تحويل تطبيق قديم للعمل داخل حاوية Docker.
  • تفعيل التخزين المؤقت للتبعيات (dependency caching) لوحدات Node.js.
  • تمكين التحديث المباشر (live-reload) باستخدام وحدات تخزين Docker.
  • تجميع جميع الخدمات ضمن ملف docker-compose واحد.

المتطلبات الأساسية

لتحقيق أقصى استفادة من هذا الدليل، ستقوم باستنساخ مشروع موجود لتطبيق جميع الأمثلة عمليًا. قبل البدء في كتابة الأكواد، تأكد من تثبيت الأدوات التالية على جهازك:

  • Docker و Docker Compose.
  • Node.js إصدار 10 أو أحدث.
  • Git.

لماذا نستخدم Docker؟

تتوالى التقنيات الحديثة والمتطورة باستمرار في عالم الإنترنت. هذه التقنيات مستقرة وممتعة في التطوير والنشر، لكنها قد تكون غير متوقعة عند تشغيلها في بيئات مختلفة. لهذا السبب، ابتكر المطورون Docker للمساعدة في تقليل احتمالات الأخطاء المحتملة.
بالنسبة لي، Docker هو أحد أدواتي المفضلة التي أستخدمها يوميًا في تطبيقات سطح المكتب، الويب، وإنترنت الأشياء (IoT). لقد منحني القدرة ليس فقط على نقل التطبيقات بين بيئات مختلفة، بل أيضًا على الحفاظ على بيئة عملي المحلية نظيفة ومنظمة قدر الإمكان.
يعمل المطورون الذين يتعاملون مع التقنيات المتطورة دائمًا على كل ما هو جديد. ولكن ماذا عن التطبيقات القديمة (legacy applications)؟ هل يجب علينا إعادة كتابة كل شيء باستخدام التقنيات الحديثة؟ نعلم أن الأمر ليس بهذه البساطة. يجب أن نعمل على الجديد، ولكن أيضًا أن نحسن من التطبيقات الموجودة.
لنفترض أنك قررت الانتقال من خوادم Windows إلى خوادم Unix. كيف ستقوم بذلك؟ هل تعرف كل التبعيات التي يتطلبها تطبيقك للعمل؟ كيف يجب أن تبدو بيئة التطوير المثالية؟
لطالما سعى المطورون لزيادة إنتاجيتهم بإضافة الإضافات (plugins)، والقوالب الجاهزة (boilerplates)، وقواعد الأكواد (codebases) إلى محرراتهم/بيئات التطوير المتكاملة (IDEs)/الطرفيات (terminals). في رأيي، يجب أن تتميز أفضل بيئة تطوير بالخصائص التالية:

  • سهولة التشغيل والاختبار.
  • مستقلة عن البيئة (Environment agnostic).
  • سرعة تقييم التعديلات.
  • سهولة الاستنساخ على أي جهاز.

باتباع هذه المبادئ، سنقوم بتكوين تطبيق في الأقسام التالية من هذا المقال.
إذا لم تسمع من قبل عن التحديث المباشر (live-reload أو hot reload)، فهي ميزة تراقب التغييرات في الكود الخاص بك وتعيد تشغيل الخادم إذا لزم الأمر. هذا يجنبك الحاجة إلى إعادة تشغيل تطبيقك أو حتى إعادة بناء النظام يدويًا.

البدء العملي

أولاً، ستحتاج إلى إنشاء مجلد فارغ باسم post-docker-livereload، والذي سيكون بمثابة مساحة عملك. توجه إلى مستودع GitHub وقم باستنساخه (clone) داخل هذا المجلد.
ثانيًا، دعنا نحلل متطلبات التطبيق. إذا ألقيت نظرة على ملف README.md، ستجد بعض التعليمات التي توضح كيفية تشغيل هذا التطبيق، كما هو موضح في الصورة أدناه:
لقطة شاشة لملف README.md يوضح كيفية تشغيل التطبيق
يتطلب التطبيق Node.js إصدار 10 أو أعلى، وقاعدة بيانات MongoDB. بدلاً من تثبيت MongoDB مباشرة على جهازك المحلي، سنقوم بتثبيته باستخدام Docker. سنقوم أيضًا بتعريضه على المنفذ localhost:27017، بحيث يمكن للتطبيقات التي لا تعمل عبر Docker الوصول إليه دون الحاجة لمعرفة عنوان IP الداخلي لـ Docker.
انسخ الأمر التالي والصقه في الطرفية (terminal) الخاصة بك:

docker run --name mongodb -p 27017:27017 -d mongo:4

باستخدام الأمر أعلاه، سيتم تنزيل وتشغيل نسخة MongoDB. لاحظ أنه إذا كان لديك بالفعل نسخة بنفس الاسم، فسيظهر خطأ يفيد بأن الاسم غير صالح. في هذه الحالة، قم بتشغيل الأمر docker rm mongodb لإزالة أي نسخة سابقة، ثم أعد تشغيل الأمر docker run مرة أخرى.

التعمق في بنية التطبيق

يشير ملف README.md إلى أنك بحاجة إلى تشغيل نسخة MongoDB قبل بدء تطبيقك، بالإضافة إلى Node.js. إذا كان Node.js مثبتًا لديك، انتقل إلى مجلد nodejs-with-mongodb-api-example وقم بتشغيل الأوامر التالية:

npm i
npm run build
npm start

بعد تشغيل هذه الأوامر، يمكنك التوجه إلى المتصفح على العنوان http://localhost:3000 وسترى التطبيق يعمل كما هو موضح في الصورة المتحركة أدناه:
فيديو متحرك يوضح تشغيل التطبيق بنجاح على المتصفح
تجدر الإشارة إلى أن التطبيق يحتوي بالفعل على أمر لتفعيل التحديث المباشر (live-reload) وهو npm run dev:watch. يجب أن تعكس سيرورة العمل (pipeline) الخطوات التالية:

  1. يقوم المطور بتعديل ملفات TypeScript.
  2. يقوم TypeScript بتحويل (transpile) الملفات إلى JavaScript.
  3. يكتشف الخادم التغييرات في ملفات JavaScript ويعيد تشغيل خادم Node.js.

لذلك، فإن مزامنة الملفات مع حاويات Docker ستعكس جميع التغييرات داخل الحاوية. سيقوم الأمر npm run build:watch من التطبيق بالتقاط التغييرات وتوليد ملفات الإخراج في مجلد lib، وبالتالي فإن الأمر npm run dev:run سيعيد تشغيل الخادم في كل مرة يتم فيها اكتشاف تغيير.

تحويل التطبيقات إلى Docker (Dockerizing)

إذا كان عالم Docker جديدًا عليك تمامًا، فلا تقلق! سنقوم بتكوينه خطوة بخطوة من الصفر. ستحتاج إلى إنشاء ملفين أساسيين للبدء:

  • Dockerfile: وهو ملف “وصفة” (receipt file) يحدد كيفية تثبيت وتشغيل تطبيقك.
  • .dockerignore: وهو ملف يسرد الملفات التي لن يتم تضمينها داخل نسخة حاوية Docker.

إنشاء ملف Dockerfile

يُعد Dockerfile المفهوم الأساسي هنا. فيه تحدد الخطوات والتبعيات اللازمة لإعداد وتشغيل التطبيق. طالما أنك قد قرأت ملف README.md، سيكون من السهل تطبيق ملف الوصفة هذا. سأضع الملف كاملاً أدناه وسنتعمق في شرحه لاحقًا.
في مجلد nodejs-with-mongodb-api-example الخاص بك، أنشئ ملفًا باسم Dockerfile والصق الكود التالي:

FROM node:14-alpine
WORKDIR /src
ADD package.json /src
RUN npm i --silent
ADD . /src
RUN npm run build
CMD npm start

ماذا يحدث هنا؟

  • **السطر 1:** يستخدم الصورة الأساسية Node.js 14-alpine.
  • **من السطر 2 إلى 4:** يقوم بنسخ وتثبيت تبعيات Node.js من المضيف (host) إلى الحاوية (container). لاحظ أن الترتيب هنا مهم؛ فإضافة ملف package.json إلى مجلد src قبل استعادة التبعيات سيؤدي إلى تخزينها مؤقتًا (cache) ويمنع إعادة تثبيت الحزم في كل مرة تحتاج فيها إلى بناء صورتك.
  • **من السطر 6 إلى 7:** يقوم بتشغيل أوامر عملية التحويل البرمجي (compilation process) ثم بدء البرنامج كما هو مذكور في ملف README.md.

تجاهل الملفات غير الضرورية باستخدام .dockerignore

أنا أعمل على نظام تشغيل OSX، وستعمل حاوية Docker على نظام Linux Alpine. عند تشغيل الأمر npm install، سيتم استعادة التبعيات لبيئات محددة.
الآن، ستقوم بإنشاء ملف لتجاهل الكود الذي تم إنشاؤه من جهازك المحلي، مثل مجلدات node_modules و lib. بهذه الطريقة، عندما تنسخ جميع الملفات من الدليل الحالي إلى الحاوية، لن تحصل على إصدارات حزم غير صالحة.
في مجلد nodejs-with-mongodb-api-example، أنشئ ملفًا باسم .dockerignore والصق الكود التالي:

node_modules/
lib/

بناء صورة Docker (Docker Image)

أفضل تشغيل هذا التطبيق من المجلد الرئيسي (rootFolder). ارجع إلى مجلد post-docker-live-reload وقم بتشغيل الأوامر التالية لإعداد صورة للاستخدام لاحقًا:

docker build -t app nodejs-with-mongodb-api-example

لاحظ أن الأمر أعلاه يستخدم العلامة -t لتحديد اسم الصورة (image name)، وبعدها مباشرة، المجلد الذي يحتوي على ملف Dockerfile.

التعامل مع وحدات التخزين (Volumes)

قبل تشغيل التطبيق، دعنا نطبق بعض التعديلات لتحسين تجربتنا في حاويات Docker.
تُعد وحدات تخزين Docker Volumes ميزة لربط ومزامنة الملفات بين جهازك المحلي وبيئة Docker. يمكنك أيضًا مشاركة وحدات التخزين هذه بين الحاويات وإعادة استخدامها لتخزين التبعيات مؤقتًا.
هدفنا هو مراقبة أي تغييرات في ملفات .ts المحلية وعكس هذه التغييرات في الحاوية. على الرغم من أن الملفات ومجلد node_modules يقعان في نفس المسار، هل تتذكر عندما ذكرت أن التبعيات تختلف باختلاف أنظمة التشغيل؟ لضمان عدم تأثير بيئتنا المحلية على بيئة Docker عند مزامنة الملفات، سنقوم بعزل مجلد node_modules الخاص بالحاوية في وحدة تخزين منفصلة. وبالتالي، عند إنشاء مجلد node_modules داخل الحاوية، لن يتم إنشاء المجلد على بيئة الجهاز المحلي.
قم بتشغيل الأمر التالي في الطرفية لإنشائه:

docker volume create --name nodemodules

تشغيل وتفعيل التحديث المباشر (Live-reload)

كما تعلم، يوضح الأمر npm run dev:watch المحدد في ملف README.md كيفية تفعيل التحديث المباشر. تكمن المشكلة في أنك تقوم بالبرمجة على جهاز محلي، ويجب أن تنعكس التغييرات مباشرة في حاويتك.
بتشغيل الأوامر التالية، ستقوم بربط بيئتك المحلية بحاوية Docker، بحيث يؤثر أي تغيير في مجلد nodejs-with-mongodb-api-example على مجلد src الخاص بالحاوية.

docker run \
  --name app \
  --link mongodb \
  -e MONGO_URL=mongodb \
  -e PORT=4000 \
  -p 4000:4000 \
  -v `pwd`/nodejs-with-mongodb-api-example:/src \
  -v nodemodules:/src/node_modules \
  app npm run dev:watch

دعنا نتعمق في ما يحدث هنا:

  • --link: يمنح هذا الخيار التطبيق إذن الوصول إلى نسخة MongoDB.
  • -e: يحدد متغيرات البيئة (environment variables). كما هو مذكور في ملف README.md، يمكنك تحديد سلسلة اتصال MongoDB التي تريد الاتصال بها عن طريق تجاوز المتغير MONGO_URL. يمكنك أيضًا تجاوز المتغير PORT إذا كنت ترغب في تشغيل التطبيق على منفذ مختلف. لاحظ أن القيمة mongodb هي نفس الاسم الذي استخدمناه لإنشاء نسخة MongoDB في الأقسام السابقة، وهذه القيمة هي أيضًا اسم مستعار لعنوان IP الداخلي لنسخة MongoDB.
  • -p: يقوم بتعيين منفذ 4000 من الحاوية إلى منفذ 4000 على الجهاز المضيف.
  • -v (الأول): يقوم بربط الدليل الحالي بحاوية Docker باستخدام وحدة تخزين افتراضية. باستخدام الأمر pwd، يمكنك الحصول على المسار المطلق لدليل العمل الحالي الخاص بك، ثم المجلد الذي تريد مزامنته مع حاوية Docker. الجزء :/src يعني أن المسار src هو تعليمات WORKDIR المحددة في Dockerfile، وبالتالي نقوم بمزامنة المجلد المحلي مع مجلد src الخاص بحاوية Docker.
  • -v (الثاني): ستقوم وحدة التخزين الثانية هنا بمزامنة وحدة التخزين الفردية التي أنشأناها مع مجلد node_modules الخاص بالحاوية.
  • app: اسم الصورة (image name).
  • npm run dev:watch: سيتجاوز هذا الأمر الأخير تعليمات CMD من ملف Dockerfile.

بعد تشغيل الأمر أعلاه، يمكنك تحديث المتصفح بعد تغيير ملف index.ts المحلي لرؤية النتائج. يوضح الفيديو أدناه هذه الخطوات عمليًا:
فيديو متحرك يوضح التحديث المباشر للتطبيق بعد تغيير الكود

تجميع الخدمات باستخدام Docker Compose

لقد أدركت الآن أن العمل بأوامر الطرفية (shell commands) فعال، ولكنه ليس الأسلوب الأكثر شيوعًا أو إنتاجية لتشغيل جميع هذه الأوامر، وبناء الصور، وإدارة النسخ يدويًا. لذا، حان الوقت لاستخدام Docker Compose!
Docker Compose هو أداة لتبسيط تجميع وربط الخدمات. يمكنك من خلاله تحديد قواعد البيانات، السجلات (logs)، التطبيقات، وحدات التخزين، الشبكات، وغيرها الكثير في ملف واحد.
أولاً، تحتاج إلى إزالة جميع النسخ النشطة لتجنب تعارض المنافذ المكشوفة. قم بتشغيل الأوامر التالية في الطرفية لإزالة وحدات التخزين، الخدمات، والحاويات:

docker rm app
docker volume rm nodemodules
docker stop $(docker ps -aq)
docker rm $(docker ps -aq)

ملف docker-compose.yml

أنشئ ملفًا باسم docker-compose.yml في مجلد post-docker-livereload باستخدام البيانات التالية:

version: '3'
services:
  mongodb:
    image: mongo:4
    ports:
      - 27017:27017
  app:
    build: nodejs-with-mongodb-api-example
    command: npm run dev:watch
    ports:
      - 4000:4000
    environment:
      MONGO_URL: mongodb
      PORT: 4000
    volumes:
      - ./nodejs-with-mongodb-api-example:/src/
      - nodemodules:/src/node_modules
    links:
      - mongodb
    depends_on:
      - mongodb
volumes:
  nodemodules: {}

يحدد الملف أعلاه الموارد حسب الأقسام. لاحظ وجود قسمي links و depends_on. حقل links هو نفس العلامة التي استخدمتها في أمر الطرفية الخاص بك. أما depends_on، فيضمن أن MongoDB هو تبعية لتشغيل تطبيقك، وسيقوم بتشغيل MongoDB قبل تطبيقك تلقائيًا!
بالعودة إلى الطرفية، لبدء جميع الخدمات وبناء صورة Docker وإنشاء وحدات التخزين وربط الخدمات، قم بتشغيل الأمر التالي:

docker-compose up --build

إذا احتجت إلى إزالة جميع الخدمات التي تم إنشاؤها مسبقًا بواسطة ملف Dockerfile هذا، يمكنك أيضًا تشغيل الأمر docker-compose down.

Docker: رفيقك في التطوير

بالفعل، Docker هو رفيقك المخلص! يمكن أن يساعدك Docker في منع العديد من الأخطاء المحتملة قبل حدوثها. يمكنك استخدامه لتطبيقات الواجهة الأمامية (front-end) والخلفية (back-end). وحتى في تطبيقات إنترنت الأشياء (IoT) عندما تحتاج إلى التحكم في الأجهزة، يمكنك تحديد سياسات لاستخدامه.
كخطوات تالية، أوصي بشدة أن تلقي نظرة على أدوات تنسيق الحاويات (container orchestrators) مثل Kubernetes و Docker Swarm. يمكن لهذه الأدوات أن تحسن تطبيقاتك الحالية بشكل أكبر وتساعدك على الانتقال إلى المستوى التالي من الاحترافية.

نشكرك على وقتك الثمين في قراءة هذا المقال. نأمل أن يكون هذا المحتوى قد قدم لك قيمة تتجاوز مجرد الكلمات، وأن يكون قد ساعدك لتصبح مفكرًا ومبرمجًا أفضل.
تابعنا على منصات التواصل الاجتماعي للحصول على المزيد من المحتوى التقني القيّم.

الخلاصة التقنية

يُظهر هذا المقال بوضوح كيف يمكن لـ Docker، بالاقتران مع ميزات مثل Docker Volumes و docker-compose، أن يحول بيئة التطوير من عملية معقدة وبطيئة إلى تجربة سلسة وفعالة للغاية. التحديث المباشر (live-reload) ليس مجرد رفاهية، بل هو ضرورة لزيادة إنتاجية المطورين، حيث يقلل بشكل كبير من الوقت المستغرق في إعادة البناء والتشغيل اليدوي. عزل التبعيات وتوحيد البيئات يضمن أن ما يعمل في بيئة التطوير سيعمل بنفس الكفاءة في بيئة الإنتاج، مما يقلل من “مشاكل بيئتي” الشهيرة. إن تبني هذه الممارسات لا يعزز فقط سرعة التطوير، بل يرفع أيضًا من جودة وموثوقية التطبيقات النهائية.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *