استخدمنا في جزئية سابقة من هذه السلسلة صورتين مختلفتين هما "ubuntu" و "node" ونفّذنا بعض الأعمال يدويًا لتشغيل تطبيق "Hello, World". ستساعدنا الأدوات والأوامر التي تعلمناها سابقًا في هذه الجزئية من السلسلة، إذ نتعلم فيه بناء الصور وتهيئة بيئة عمل التطبيق. سنبدأ ببناء واجهة خلفية نمطية باستخدام Express/Node.js ثم نبني عليها مستخدمين خدمات أخرى مثل قواعد بيانات MongoDB.
ملفات Dockerfile
بإمكاننا إنشاء صورة جديدة تتضمن التطبيق "!Hello, World" بدلًا من تعديل الحاوية بنسخ ملفات جديدة إليها، وتساعدنا في ذلك أداة تُدعى Dockerfile، وهي ملفٌ نصي بسيط يحتوي كل التعليمات الخاصة بإنشاء صورة. سنبدأ إذًا بإنشاء مثال عن Dockerfile من تطبيق "!Hello, World".
أنشئ مجلدًا جديدًا على جهازك ثم أنشئ ضمنه الملف "Dockerfile " إن لم تكن قد فعلت ذلك مسبقًا. ولنضع كذلك الملف "index.js" الذي يضم الشيفرة ('!console.log('Hello, World
إلى جواره. ستبدو هيكلية المجلد على النحو التالي:
├── index.js └── Dockerfile
سنخبر الصورة من خلال "Dockerfile" بثلاثة أمور:
- استخدم node:16 أساسًا للصورة.
- ضع الملف "index.js" ضمن الصورة، كي لا نُضطر إلى نسخه يديويًا إلى الحاوية.
- استخدم node لتنفيذ شيفرة الملف "index.js" عندما نشغّل الحاوية من الصورة.
توضع هذه النقاط الثلاث ضمن ملف "Dockerfile" وأفضل مكان لإنشاء هذا الملف هو جذر المشروع، وسيبدو هذا الملف على النحو التالي:
FROM node:16 WORKDIR /usr/src/app COPY ./index.js ./index.js CMD node index.js
-
FROM
: تخبر هذه التعليمة برنامج دوكر Docker أنّ أساس الصورة هوnode:16
. -
COPY
: تنسخ هذه التعليمة الملفindex.js
من الجهاز المضيف إلى ملف بنفس الاسم ضمن الصورة. -
CMD
: تخبر هذه التعليمة البرنامج ما يجب أن يحدث عند تنفيذ الأمرdocker run
. وهذه التعليمة هي تعليمة تنفيذ افتراضية يمكن استبدالها بالمعامل الذي يُعطي بعد اسم الصورة. اكتب الأمرdocker run --help
إن نسيت. -
WORKDIR
: وضعت هذه التعليمة للتأكد من أننا لن نفعل شيئًا يتداخل مع محتوى الحاوية. إذ سيضمن أنّ كل التعليمات التي ستليه ستكون ضمن المجلد "usr/src/app/" الذي يُعد مجلد العمل في هذه الحالة. فإن لم يكن هذا المجلد موجودًا في الصورة، سيُنشأ تدريجيًا.
لم نحدد مجلد عمل WORKDIR
، فقد نجازف بتغيير ملفات هامة بطريق الخطأ. لو تحققت من الجذر /
للصورة بتنفيذ الأمر docker run node:16 ls
ستجد العديد من المجلدات والملفات في هذه الصورة.
نستطيع الآن استخدام الأمر docker build
لبناء الصورة بناءً على ملف Dockerfile، لكننا سنضيف الراية t-
إلى هذا الأمر كي تساعدنا في إعادة تسمية الصورة:
$ docker build -t fs-hello-world . [+] Building 3.9s (8/8) FINISHED ...
ستكون نتيجة تنفيذ الأمر هي:
"docker please build with tag fs-hello-world the Dockerfile in this directory"
والتي تشير إلى بناء صورة بالاسم "fs-hello-world" بالاعتماد على ملف Dockerfile الموجود في المجلد. يمكنك الإشارة إلى أي ملف Dockerfile لكن في حالتنا البسيطة يكفي وضع .
للإشارة إلى هذا الملف ضمن المجلد لهذا انتهى الأمر بالنقطة.
يمكنك تشغيل الحاوية الآن باستخدام الأمر docker run fs-hello-world
ويمكن نقل الصورة أو تنزيلها أو حذفها فهي في طبيعتها ملفات. إذ يمكنك تشكيل قائمة بالصور التي يضمها حاسوبك باستخدام الأمر docker image ls
أو حذف الصورة docker image rm
. اطلع على بقية الأوامر المتاحة بتنفيذ أمر المساعدة docker image --help
.
صور أكثر فائدة
ينبغي أن يكون نقل خادم Express إلى حاوية ببساطة نقل تطبيق "!Hello, World"، إذ يكمن الاختلاف الوحيد بين الحالتين في وجود ملفات أكثر في حالة الخادم، لكن ستغدو الأمور أبسط بوجود التعليمة COPY
.
لنحذف الآن الملف "index.js" وننشئ خادم Express باستخدام express-generator الذي يساعدنا على بناء هيكلية بسيطة للتطبيق.
$ npx express-generator ... install dependencies: $ npm install run the app: $ DEBUG=playground:* npm start
لنشغل التطبيق الآن كي نرى ما فعلنا، وانتبه أن أمر التشغيل قد يختلف في جهازك، فالمجلد في المثال السابق يُدعى "playground".
$ npm install $ DEBUG=playground:* npm start playground:server Listening on port 3000 +0ms
يمكنك الانتقال الآن إلى العنوان "http://localhost:3000" حيث يعمل التطبيق.
إن ضم الملفات في حاويات هي عملية سهلة نوعًا ما بناءً على ما واجهناه حتى الآن:
- استخدم الأساس node.
- اضبط مجلد العمل كي لا يتداخل عملك مع بقية محتويات الصورة.
- انسخ كل ملفات المجلد إلى الصورة.
-
ابدأ بتنفيذ الأمر
DEBUG=playground:* npm start
بعد الأمرCMD
.
لنضع ملف Dockerfile التالي في جذر المشروع:
FROM node:16 WORKDIR /usr/src/app COPY . . CMD DEBUG=playground:* npm start
سنبني الآن الصورة انطلاقًا من ملف باستخدام الأمر:
docker build -t express-server .
وسنشغلها باستخدام الأمر:
docker run -p 3123:3000 express-server
تبلّغ الراية p-
دوكر بضرورة فتح منفذ الجهاز المضيف وتوجيهه إلى منفذ للحاوية ولهذا الأمر الصيغة التالية: p host-port:application-port-
$ docker run -p 3123:3000 express-server > playground@0.0.0 start > node ./bin/www Tue, 29 Jun 2021 10:55:10 GMT playground:server Listening on port 3000
إن لم يفلح الأمر، تجاوز الفقرة التالية، فهناك تفسير لعدم نجاح الأمر حتى لو اتبعت الخطوات السابقة تمامًا.
سيبدأ التطبيق عمله الآن، لهذا سنختبره بإرسال الطلب GET إلى العنوان "/http://localhost:3123". بالنسبة لإيقاف الخادم فهو أمر عصيبٌ حاليًا، لهذا افتح نافذة أخرى للطرفية ونفذ الأمر docker kill
لإيقاف التطبيق؛ إذ يرسل هذا الأمر الإشارة SIGKILL إلى التطبيق ويجبره على الإنهاء. ستحتاج إلى اسم أو معرّف الحاوية ID مثل وسيط لتنفيذ الأمر السابق. وتجدر الإشارة أنه يكفي استخدام بداية المعرّف id عند تمريره وسيطًا، إذ سيعرف دوكر مباشرةً الحاوية المقصودة.
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 48096ca3ffec express-server "docker-entrypoint.s…" 9 seconds ago Up 6 seconds 0.0.0.0:3123->3000/tcp, :::3123->3000/tcp infallible_booth $ docker kill 48 48
لنعمل من الآن وصاعدًا على نفس المنفذ في كلا الجانبين p-
. وهكذا لن تضطر إلى تذكُّر ما المنفذ الذي عليك اختياره.
إصلاح المشاكل المحتملة الناتجة عن عملية النسخ واللصق
لا بُد من تغيير بعض الخطوات لإنشاء ملف Dockerfile متقدم، وقد لا يعمل المثال الذي أوردناه سابقًا على الإطلاق، لأننا أهملنا خطوة هامة.
عندما تنفِّذ الأمر npm install
على حاسوبك، فقد يُثبّت مدير حزم Node بعض الاعتمادات التي تتعلق بنظام التشغيل أثناء تقدم التثبيت. وقد ننقل صدفةً أجزاءً غير وظيفية إلى الصورة عند استخدام التعليمة COPY
، ويحدث ذلك بسهولة إن نسخنا المجلد "node_modules" إلى الصورة.
من المهم جدًا إبقاء تلك النقاط في ذاكرتنا عند بناء الصورة، فمن الأفضل أن ننفذ معظم الأعمال مثل npm install
أثناء عملية البناء ضمن الحاوية بدلًا من تنفيذها قبل البناء؛ إذ أن القاعدة الجوهرية هنا هي نسخ الملفات التي ستدفعها إلى غيت هب GitHub فقط، ولا ينبغي نسخ الاعتماديات ومتطلبات البناء كونها أشياء يمكن تثبيتها أثناء بناء الصورة.
يمكنك استخدام الملف "dockerignore." لحل المشكلة، وهو ملفٌ شبيه بملف التجاهل "gitignore." لمنع نسخ الملفات غير المطلوبة إلى الصورة. يُوضع هذا الملف إلى جوار الملف Dockerfile، وإليك مثالًا عن محتوياته:
.dockerignore .gitignore node_modules Dockerfile
لكننا سنحتاج إضافةً إلى ملف "dockerignore." في حالتنا إلى تثبيت الاعتماديات خلال خطوة البناء، لهذا سيتغير ملف Dockerfile إلى الشكل:
FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm install CMD DEBUG=playground:* npm start
قد يكون تنفيذ الأمر npm install
خطرًا، لهذا يزوّدنا npm بأداة أفضل لتثبيت الاعتماديات وهو الأمر ci
.
تُلخّص الاختلافات بين ci
و install
على النحو التالي :
-
قد يُحدّث
install
الملف "package-lock.json". -
قد يُثبِّت
install
نسخةً مختلفةً من الاعتمادية إن ظهرت المحارف "^" أو "~" في نسخة الاعتمادية. -
سيحذف
ci
المجلد "node_modules" قبل تثبيت أي شيء. -
سيتبع
ci
الملف "package-lock.json" ولا يبدّل أي ملف.
باختصار: يقدم ci
نسخًا يمكن الاعتماد عليها، بينما يُستخدم install
عند تثبيت اعتماديات جديدة.
طالما أننا لن نثبِّت أي شيء جديد في خطوة البناء، ولا نريد تغييرات فجائية في النسخ، سنستخدم الأمر ci
:
FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci CMD DEBUG=playground:* npm start
كما يمكننا تحسين الحالة أكثر باستخدام الأمر npm ci --only=production
كي لا نهدر الوقت في تثبيت الاعتماديات.
سيحذف ci
المجلد "node_modules" كما أشرنا قبل قليل، وبالتالي لن نضطر إلى إنشاء الملف "dockerignore.". مع ذلك، يُعد هذا الملف أداةً رائعةً عندما تريد تحسين عملية البناء، وسنتحدث باختصار عن هذا الموضوع لاحقًا.
ينبغي أن يعمل الملف من جديد، لهذا حاول تنفيذ الأمر التالي:
docker build -t express-server . && docker run -p 3000:3000 express-server
لاحظ كيف وصلنا هنا أمري باش bash باستخدام &&
، وسنحصل تقريبًا على نفس النتيجة إذا نفذنا كلا الأمرين كلًّا على حدة؛ لكن عندما تربط أمرين باستخدام &&
، فلن يُنفَّذ الأمر الآخر إذا فشل تنفيذ أحدهما.
ضبطنا سابقًا متغير البيئة :DEBUG=playground
من خلال الأمر CMD
لتشغيل npm، كما يمكننا أيضًا استخدام التعليمة ENV
في ملف Dockerfiles لضبط متغيرات البيئة، فلنفعل ذلك:
FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci ENV DEBUG=playground:* CMD npm start
أفضل الممارسات المتعلقة باستخدام Dockerfiles
عليك اتباع القاعدتين الجوهريّتين التاليتين عند إنشاء الصور:
- حاول أن تبني صورةً آمنة قدر المستطاع.
- حاول أن تُنشئ صورةً صغيرة قدر الإمكان.
تُعد الصور الأصغر أكثر أمانًا لأن مجال الهجوم عليها محدود، كما يمكن نقلها بسرعة أكبر ضمن أنابيب النشر.
وأخيرًا لا بُد من إصلاح آخر نقطة أهملناها وهي تشغيل التطبيق مثل جذر بدلًا من مستخدم بصلاحيات منخفضة:
FROM node:16 WORKDIR /usr/src/app COPY --chown=node:node . . RUN npm ci ENV DEBUG=playground:* USER node CMD npm start
التمرين 12.5: إنشاء حاوية لتطبيق Node
يضم المستودع الذي نسخته في التمرين الأول تطبيق لائحة مهام todo-app. اطلع على الواجهة الخلفية "todo-app/todo-backend" للتطبيق واقرأ الملف "اقرأني README". لن نقترب حاليًا من الواجهة الأمامية "todo-frontend"
الخطوة الأولى: وضع الواجهة الخلفية "todo-backend" ضمن حاوية بإنشاء الملف "todo-app/todo-backend/Dockerfile" ثم بناء الصورة.
الخطوة الثانية: تشغيل الصورة على المنفذ الصحيح، والتأكد أن عدّاد الزيارات سيزداد عند استخدامه عبر المتصفح على العنوان "/http://localhost:3000" (أو على أي منفذ آخر قد تهيّئه).
تلميح: شغّل التطبيق خارج الحاوية أولًا للتحقق منه قبل وضعه في الحاوية.
استخدام الأداة docker-compose
أنشأنا في جزئية سابقة من هذه السلسلة خادمًا وعلمنا أنه يعمل على المنفذ 3000 وشغّلناه باستخدام الأمر:
docker build -t express-server . && docker run -p 3000:3000 express-server
ويبدو أننا سنحتاج إلى سكربت لتذكر هذه التعليمات، لكن لحسن الحظ يقدّم دوكر لنا حلًا أفضل.
تُعد الأداة Docker-compose من الأدوات الرائعة الأخرى التي تساعدك على إدارة الحاوية، لهذا سنبدأ استخدام هذه الأداة خلال رحلتنا في دراسة الحاويات، إذ ستساعد على توفير بعض الوقت عند تهيئة الحاوية.
ثبّت الأداة Docker-compose ثم تأكد من عملها على النحو التالي:
$ docker-compose -v docker-compose version 1.29.2, build 5becea4c
سنحوّل الآن الأوامر السابقة إلى ملف yaml يمكن تخزينه في مستودع غيت Git.
أنشئ الملف "docker-compose.yml" وضعه في جذر المشروع إلى جوار ملف Dockerfile، ثم ضع المحتوى التالي ضمنه:
version: '3.8' # نسخة جديدة لا بد أن تعمل services: app: # اسم الخدمة وقد يكون أي شيء image: express-server # صرّح عن الصورة التي تريد استخدامها build: . # حدد مكان بناء الصورة إن لم تكن موجودة ports: # حدد المنفذ الذي يرتبط به التطبيق - 3000:3000
وضعنا شرحًا لكل سطر إلى جواره، لكن إذا أردت معرفة المواصفات الكاملة فعُد إلى التوثيق.
سنتمكن الآن من استخدام الأمر docker-compose up
في بناء وتشغيل التطبيق، وإن أردت إعادة بناء الصور، استخدم الأمر docker-compose up --build
.
بإمكانك أيضًا تشغيل التطبيق في الخلفية باستخدام الأمر docker-compose up -d
(الراية d-
لفصل التطبيق) وإيقافه بتنفيذ الأمر docker-compose down
.
يُصرِّح إنشاء الملفات بهذه الطريقة عمّا تريده بدلًا من ملفات السكربت التي عليك تنفيذها وفق ترتيبٍ محدد أو عددٍ محددٍ من المرات، وهذه ممارسةٌ جيدةٌ جدًا.
التمرين 12.6: الأداة docker-compose
أنشئ الملف "todo-app/todo-backend/docker-compose.yml" الذي يعمل مع تطبيق node من التمرين السابق. وعليك الانتباه إلى عدّاد الزيارات فهو الميزة الوحيدة التي ينبغي أن تعمل.
استخدام الحاويات في مرحلة التطوير
يمكن استخدام الحاويات أثناء تطوير التطبيقات بطرق متعددة لتسهيل عملك، ومن إحدى فوائدها تجاوز تثبيت وتهيئة الأدوات مرتين.
قد لا يكون خيارك الأفضل أن تنقل كامل بيئة التطوير إلى الحاوية، لكن إذا أردت ذلك فهذا ممكن. سنعود إلى هذه الفكرة في آخر القسم، لكن حتى ذلك الوقت عليك تشغيل تطبيق node بنفسه خارج الحاويات.
يستخدم التطبيق الذي تعرفنا عليه في التمارين السابقة MongoDB، لهذا دعونا نستخدم Docker Hub لإيجاد صورة MongoDB، إذ أنه المكان الافتراضي الذي نسحب الصور منه، كما يمكنك استخدام مسجلات أخرى أيضًا، لكن طالما أننا نتعمق في دوكر فهو خيارٌ جيد. يمكنك أن تجد من خلال بحث سريع الصورة المطلوبة على العنوان https://hub.docker.com/_/mongo.
أنشئ ملف yaml يُدعى "todo-app/todo-backend/docker-compose.dev.yml" يحتوي ما يلي:
version: '3.8' services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database
يُوضَّح معنى أول متغيري بيئة معرفين في الشيفرة السابقة في صفحة Docker Hub:
"تُنشِئ المتغيرات المُستخدمة على التوازي مستخدمًا جديدًا وتضبط كلمة المرور له. يُنشأ هذا المستخدم في قاعدة بيانات إدارة الاستيثاق ويعطى دور الجذر root، وهو دور المستخدم الأعلى superuser."
يخبر متغير البيئة الأخير MONGO_INITDB_DATABASE
قاعدة البيانات MongoDB أن تنشئ قاعدة بيانات بهذا الاسم. بإمكانك استخدام الراية f-
لتخصيص ملف لتشغيل أمر Docker Compose مثل:
docker-compose -f docker-compose.dev.yml up
شغًل الآن MongoDB من خلال الأمر:
docker-compose -f docker-compose.dev.yml up -d
أما الراية d-
فلتشغيل العملية في الخلفية.
يمكنك متابعة سجلات الخرج بتنفيذ الأمر:
docker-compose -f docker-compose.dev.yml logs -f
وتُستخدم الراية f-
للتأكد من متابعة السجلات.
لا نحتاج حاليًا لتشغيل تطبيق Node ضمن الحاوية، فهذا أمرٌ ينطوي على قدر من التحدي، لكننا سنكتشف هذا الخيار في آخر قسم.
نفِّذ الأمر القديم npm install
على جهازك لإعداد تطبيق Node، ثم شغل التطبيق باستخدام متغيرات البيئة اللازمة. يمكنك تعديل الشيفرة لجعل متغيرات البيئة متغيرات افتراضية أو استخدم الملف env.
. لا ضرر من وضع هذه المفاتيح على غيت هب لأنها تُستخدم ضمن بيئة التطوير المحلية، لذلك سأضعها هناك عن طريق الأمر npm run dev
لمساعدتك في النسخ واللصق.
$ MONGO_URL=mongodb://localhost:3456/the_database npm run dev
لن يكون ذلك كافيًا، بل نحتاج إلى إنشاء مستخدم نستوثِق منه ضمن الحاوية، إذ سيقود الولوج إلى العنوان " http://localhost:3000/todos" إلى خطأ في الاستيثاق.
[nodemon] 2.0.12 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node ./bin/www` (node:37616) UnhandledPromiseRejectionWarning: MongoError: command find requires authentication at MessageStream.messageHandler (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/mongodb/lib/cmap/connection.js:272:20) at MessageStream.emit (events.js:314:20)
ربط وتركيب وتهيئة قاعدة البيانات
ستجد في صفحة MongoDB Docker Hub تحت عنوان " تهيئة نسخة جديدة Initializing a fresh instance" معلومات عن تنفيذ شيفرة لتهيئة قاعدة البيانات ومستخدم لها.
في المشروع التجريبي ملف "todo-app/todo-backend/mongo/mongo-init.js" يضم المحتوى التالي:
db.createUser({ user: 'the_username', pwd: 'the_password', roles: [ { role: 'dbOwner', db: 'the_database', }, ], }); db.createCollection('todos'); db.todos.insert({ text: 'Write code', done: true }); db.todos.insert({ text: 'Learn about containers', done: false });
يهيئ الملف قاعدة البيانات مع مستخدم وبعض المهام المخزّنة في القاعدة، وعلينا في الخطوة التالية نقلها إلى الحاوية وتشغيلها.
من الممكن إنشاء صورة جديدة من mongo ثم نسخ الملف إلى الداخل، أو استخدام الأمر لتركيب الملف ضمن الحاوية، لكننا سنختار الطريقة الأخيرة.
إن التركيب بالربط Bind mount هي عملية ربط ملف على حاسوبك المحلي بملف في الحاوية، ويمكننا تنفيذ ذلك باستخدام الراية v-
مع الأمر بالصيغة التالية: v FILE-IN-HOST:FILE-IN-CONTAINER-
. يُعرّف التركيب بالربط تحت المفتاح volumes
في الملف "docker-compose"، أما صياغة التصريح فتكون على النحو التالي "مضيف ثم حاوية":
mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
إن النتيجة هي أنّ الملف "mongo-init.js" في مجلد mongo على حاسوبك سيكون نفسه الملف "mongo-init.js" في المجلد "docker-entrypoint-initdb.d/" من الحاوية، وسيؤدي تعديل أحدهما إلى تعديل الآخر. لا حاجة لتغيير أي شيء أثناء التشغيل، وهذا هو مفتاح تطوير البرمجيات ضمن الحاويات.
نفّذ الأمر التالي للتأكد من وجود كل شيء في مكانه:
docker-compose -f docker-compose.dev.yml down --volumes
ثم ابدأ لائحة جديدة بتنفيذ الأمر التالي لتهيئة قاعدة البيانات:
docker-compose -f docker-compose.dev.yml up
إذا واجهك خطأ على النحو التالي:
mongo_database | failed to load: /docker-entrypoint-initdb.d/mongo-init.js mongo_database | exiting with code -3
فقد يكون لديك مشكلةً في إذن القراءة، وهذا أمرٌ قد يحدث عند التعامل مع الأقراص volumes. بإمكانك في حالتنا استخدام الأمر chmod a+r mongo-init.js
الذي يمنح أيًا كان إمكانية قراءة الملف، لكن كن حذرًا عند استخدام التعليمة chmod
لأن السماح بإذونات أكبر قد يقود إلى مشاكل أمنية، لهذا استخدم تلك التعليمة على الملف "mongo-init.js" الموجود على جهازك فقط.
سيعمل تطبيق express الآن عبر متغيرات البيئة الصحيحة:
$ MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database npm run dev
لنتأكد أن الطلب إلى العنوان http://localhost:3000/todos سيعيد كل المهام الموجودة في قاعدة البيانات، فمن المفترض أن يعيد المهمتان اللتان هيأناهما، ولا بد من استخدام Postman لاختبار وظائف التطبيق الأساسية مثل إضافة وحذف مهام من قاعدة البيانات.
البيانات المقيمة في الأقراص
لا تُخزّن الحاويات بياناتنا افتراضيًا، فعندما تغلق حاوية قد تستطيع أو لا تستطيع استعادة البيانات. وبصورةٍ عامة هناك طريقتان مختلفتان لتخزين البيانات:
- التصريح عن مكان ضمن منظومة الملفات (عملية الربط بالتركيب bind mount).
- ترك الأمر لبرنامج دوكر كي يقرر تخزين البيانات (استخدام الأقراص volume)
يُفضّل الخيار الأول عادةً في معظم الحالات التي تحتاج فيها حقًا إلى تفادي حذف البيانات، وسنرى الأسلوبين بالتطبيق العملي:
services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - ./mongo_data:/data/db
ستُنشئ الإعدادات السابقة مجلدًا يُدعى mongo_data
ضمن منظومة الملفات في حاسوبك ثم تربطه بالحاوية بالاسم data/db/
. أي أنّ البيانات الموجودة في المجلد data/db/
ستُخزّن خارج الحاوية لكن بإمكانها الوصول إليها. وتذكر إضافة المجلد إلى ملف التجاهل "gitignore.".
يمكن تحقيق الأمر ذاته باستخدام أقراص التخزين المسماة:
services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - mongo_data:/data/db volumes: mongo_data:
يُنشأ القرص الآن ويدار من قبل دوكر، وبإمكانك قبل تشغيل التطبيق استعراض الأقراص الموجودة بتنفيذ الأمر docker volume ls
، أو فحصّها docker volume inspect
، أو حذفها docker volume rm
. ليس اختيار مكان تخزين البيانات محليًا في هذا الخيار أمرًا قليل الأهمية موازنةً بالخيار السابق.
التمرين 12.7: كتابة القليل من الشيفرة للتعامل مع MongoDB
نفترض في هذا التمرين أنك أنجزت جميع الإعدادات التي تحدثنا عنها سابقًا بعد التمرين 12.5. وسيبقى تشغيل التطبيق خارج الحاوية وستوضع قاعدة البيانات MongoDB فقط ضمن الحاوية.
لا توجد طريق ملائمة حتى الآن للحصول على مهمة واحدة (GET* /todos/:id*) وتحديث مهمة واحدة (PUT* /todos/:id*). جد حلًا لهاتين المشكلتين.
تنقيح المشاكل في الحاويات
اقتباسقد تصل أثناء كتابة الشيفرة على الأغلب إلى حالة لا يعمل فيها أي شيء. -ماتي لوكينين Matti Luukkainen.
لا بُد من تعلم استعمال بعض الأدوات لتنقيح التطبيقات ضمن الحاويات، لأننا لا نستطيع استخدام الأمر console.log
دائمًا. عندما تظهر الثغرات في شيفرتك، فلا بد وأن يعمل شيء ما في شيفرتك لتنطلق منه. وعمومًا هناك وضعان لتبدأ منهما: الأول هو تطبيق يعمل والثاني هو تطبيق لا يعمل، لهذا سنطلع على بعض الأدوات التي تساعد في تنقيح التطبيق في الحالة الثانية.
يمكن أن تنتقل أثناء تطوير البرنامج في عملك خطوة خطوة لتتاكد طوال الوقت أن كل شيء يعمل كما تتوقع. لكن لا ينطبق هذا الأمر عند ضبط الإعدادات، فقد تخفق الإعدادات التي تكتبها حتى لحظة الإنتهاء منها؛ لهذا إذا كتبت ملف "docker-compose.yml" طويل أو ملف Dockerfile ولم يعمل، عليك التروي برهة والتفكير بالطرق المختلفة التي تتأكد منها أن شيء ما يعمل بالشكل المطلوب.
لا تزال طريقة التشكيك بكل شيء واردة هنا، وكما قلنا في القسم الثالث: عليك أن تكون منظّما، وطالما أنّ المشكلة قد تكون في أي مكان، عليك التشكيك بكل شيء، وإزالة كل مصادر الخطأ واحدًا تلو الآخر. فالتوقف والتفكير بالمشكلة بدلًا من كتابة مزيدٍ من الإعدادات هي الطريقة الأنسب في الحصول على حل بسيط، كما أن البحث السريع باستخدام محركات البحث قد يساعدك في التقدم.
الأمر exec
يمكن استخدام الأمر exec للقفز مباشرةً إلى الحاوية وهي تعمل. لهذا سنهيّئ خادم ويب في الخلفية ونجري بعض الإعدادات كي نشغّله ليعرض الرسالة "!Hello, exec" ضمن المتصفح. سنستخدم الخادم Nginx القادر على تخديم الملفات الساكنة، ويدعم الملف "index.html" الذي يمثل الصفحة الافتراضية ويسمح لنا بتعديلها أو استبدالها:
$ docker container run -d nginx
الأسئلة المطروحة الآن هي:
- أين سنتوجه عبر المتصفح؟
- هل يعمل الخادم بالفعل؟
لكن نعرف كيف نجيب على هذه الأسئلة وذلك باستعراض الحاويات التي تعمل:
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3f831a57b7cc nginx "/docker-entrypoint.…" About a minute ago Up About a minute 80/tcp keen_darwin
لقد حصلنا على جواب السؤال الأول فعلًا، فيبدو أن الخادم ينصت إلى المنفذ 80. لهذا سنطفئ الخادم ثم نعيد تشغيله مستخدمين الراية p-
للسماح للمتصفح بالوصول إليه:
$ docker container stop keen_darwin $ docker container rm keen_darwin $ docker container run -d -p 8080:80 nginx
لنلقي نظرةً على التطبيق بالانتقال إلى العنوان http://localhost:8080، وستجد أنّ التطبيق يعرض رسالة خاطئة، لهذا سنقفز إلى الحاوية مباشرةً لإصلاحها. أبقِ متصفحك مفتوحًا، فلا نريد إغلاق الحاوية عند إصلاحها، بل سنستخدم أوامر الطرفية ضمن الحاوية، ولا تنسى استخدام الراية it-
لضمان قدرتك على التفاعل مع الحاوية:
$ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7edcb36aff08 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8080->80/tcp, :::8080->80/tcp wonderful_ramanujan $ docker exec -it wonderful_ramanujan bash root@7edcb36aff08:/#
بعد أن قفزنا داخل الحاوية، لا بد من إيجاد الملف الخاطئ واستبداله، إذ يتضح لنا من خلال بحث سريع باستخدام محرك بحث أن الملف هو "usr/share/nginx/html/index.html/".
لننتقل إلى المجلد ونحذف الملف:
root@7edcb36aff08:/# cd /usr/share/nginx/html/ root@7edcb36aff08:/# rm index.html
لو انتقلنا الآن إلى العنوان http://localhost:8080 فسنحصل على الصفحة 404 لأننا حذفنا الملف الصحيح، لهذا سنستبدله بملف آخر يضم المحتوى الصحيح:
root@7edcb36aff08:/# echo "Hello, exec!" > index.html
أعد تحميل الصفحة وسترى كيف يعرض المتصفح الرسالة الصحيحة. وهكذا نرى كيف نستفيد من الأمر exec
في التفاعل مع الحاوية. ستُفقد كل التغييرات التي أجريتها عند حذف الحاوية، ولتحفظ هذه التغييرات لا بد من دفعها باستخدام الأمر commit
.
التمرين 12.8: واجهة سطر أوامر Mongo
استخدم script
لتسجيل ما تفعله، ثم احفظ الملف بالاسم "script-answers/exercise12_8.txt".
حاول الولوج إلى قاعدة البيانات في التمرين السابق أثناء تشغيل MongoDB باستخدام سطر أوامر CLI. يمكنك تنفيذ الأمر باستخدام exec
، ثم أضف بعد ذلك مهمة جديدة عبر سطر الأوامر CLI. شغّل سطر الأوامر وأنت ضمن الحاوية باستخدام الأمر mongo
.
يتطلب سطر أوامر mongo رايتي اسم مستخدم وكلمة مرور للاستيثاق على نحوٍ صحيح،. وستعمل الرايات u root -p example-
جيدًا، وتكون القيم مأخوذةً من الملف docker-compose.dev.yml.
- الخطوة الأولى: شغّل MongoDB.
-
الخطوة الثانية: استخدم الأمر
exec
للدخول إلى الحاوية. - الخطوة الثالثة: افتح واجهة سطر أوامر mongo.
- عندما تصل إلى واجهة سطر أوامر mongo يمكنك أن تطلب منها عرض قواعد البيانات:
> show dbs admin 0.000GB config 0.000GB local 0.000GB the_database 0.000GB
للولوج إلى قاعدة البيانات الصحيحة:
> use the_database
ولإيجاد المجموعات:
> show collections todos
يمكنك الآن الوصول إلى البيانات الموجودة ضمن تلك المجموعات:
> db.todos.find({}) { "_id" : ObjectId("611e54b688ddbb7e84d3c46b"), "text" : "Write code", "done" : true } { "_id" : ObjectId("611e54b688ddbb7e84d3c46c"), "text" : "Learn about containers", "done" : false }
أضف مهمةً جديدة نصها: "Increase the number of tools in my toolbelt" مع ضبط حالتها على false
. راجع التوثيق لتتعلم إضافة المدخلات إلى قاعدة البيانات، وتأكد من رؤية المهمة الجديدة في تطبيق Express وعند الاستعلام عنه عبر سطر أوامر Mongo CLI.
قاعدة البيانات Redis
Redis هي قاعدة بيانات من الشكل (مفتاح-قيمة) مما يجعلها قاعدة لحفظ البيانات بهيكلية أدنى من MongoDB مثلًا، فلن تجد فيها مجموعات أو جداول بل كميات من البيانات يمكن الحصول عليها وفقًا لقيم المفاتيح المرتبط بالبيانات (القيم).
تعمل قاعدة البيانات Redis ضمن الذاكرة المؤقتة أي أنها لا تحتفظ بالبيانات دائمًا، ومن أفضل طرق الاستفادة منها هي استخدامها مثل مخزن مؤقت لتطبيق cache، إذ تُستخدم المخازن المؤقتة غالبًا لتخزين البيانات التي قد تكون بطيئة عند إحضارها وحفظها حتى تفقد صلاحيتها فيتوجب عليك إحضارها مجددًا بعد ذلك وتخزينها وهكذا.
لا علاقة لقاعدة البيانات Redis بالحاويات لكن وطالما أننا قادرين على إضافة أي خدمة مصدرها طرف آخر إلى التطبيق، فلماذا لا نتعلم شيئًا جديدًا؟
التمارين 12.9 - 12.11
التمرين 12.9: إعداد Redis للمشروع
هُيّئ خادم Express مسبقًا للتعامل مع Redis ويفتقد فقط إلى متغير البيئة REDIS_URL
، إذ يستخدم التطبيق متغير البيئة للاتصال بقاعدة البيانات. اطلع على عمل Redis مع Docker Hub ثم أضفها إلى الملف "todo-app/todo-backend/docker-compose.dev.yml" من خلال تعريف خدمة جديدة بعد mogo.
services: mongo: ... redis: ???
بما أن صفحة Docker Hub لا تضم جميع المعلومات الكافية، استخدم غوغل مثلًا في البحث. ستجد المنفذ الافتراضي للقاعدة بإجراء البحث الموضح في الصورة التالية:
لا نعلم بعد إن كان الإعداد سيعمل ما لم نجرّب. لن يستخدم التطبيق Redis من تلقاء نفسه طبعًا، وهذا ما سنراه في التمرين التالي.
حالما تهيئ Redis وتشغله، أعد تشغيل الواجهة الخلفية ومرر لها متغير البيئة REDIS_URL
بالصيغة redis://host:port
.
$ REDIS_URL=insert-redis-url-here MONGO_URL=mongodb://localhost:3456/the_database npm run dev
يمكنك الآن اختبار الإعداد بإضافة السطر التالي:
const redis = require('../redis')
إلى مثال خادم Express في الملف "routes/index.js". إن لم يحدث شيء فقد طُبِّق الإعداد بصورةٍ صحيحة وإلا سينهار الخادم:
events.js:291 throw er; // Unhandled 'error' event ^ Error: Redis connection to localhost:637 failed - connect ECONNREFUSED 127.0.0.1:6379 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) Emitted 'error' event on RedisClient instance at: at RedisClient.on_error (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:342:14) at Socket.<anonymous> (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:223:14) at Socket.emit (events.js:314:20) at emitErrorNT (internal/streams/destroy.js:100:8) at emitErrorCloseNT (internal/streams/destroy.js:68:3) at processTicksAndRejections (internal/process/task_queues.js:80:21) { errno: -61, code: 'ECONNREFUSED', syscall: 'connect', address: '127.0.0.1', port: 6379 } [nodemon] app crashed - waiting for file changes before starting...
التمرين 12.10
ثُبتت قاعدة البيانات https://www.npmjs.com/package/redis في المشروع مسبقًا وأضيفت إليه دالتين تعيدان وعودًا وهما getAsync
و setAsync
:
-
تقبل الدالة
setAsync
قيمةً ومفتاحًا، ويُستخدم المفتاح لتخزين القيمة. -
تقبل الدالة
getAsync
مفتاحًا وتعيد قيمته المقابلة في الوعد promise.
أنجز عدادًا للمهام يخزّن عدد المهام التي تُنشئها في قاعدة البيانات Redis:
- الخطوة الأولى: زد العداد بمقدار واحد في أي لحظة يُرسل فيها طلب لإضافة مهمة.
-
الخطوة الثانية: أنشئ وصلة
GET/statics
ساكنة يمكنك طلب معلومات وصفية منها، وينبغي أن تُعاد بصيغة JSON على النحو التالي:
{ "added_todos": 0 }
التمرين 12.11
استخدم script
لتسجيل ما تفعله، ثم تحفظ الملف بالاسم "script-answers/exercise12_11.txt".
إن لم يسلك التطبيق السلوك المطلوب، فقد يساعدك الولوج المباشر إلى قاعدة البيانات في الدلالة على المشاكل. لنلقي نظرةً إذًا على طريقة استخدام واجهة سطر أوامر Redis في الوصول إلى قاعدة البيانات:
-
انتقل إلى حاوية redis باستخدام الأمر
docker exec
، ثم افتح الواجهة redis-cli. -
ابحث عن المفتاح الذي استخدمته باستخدام الأمر
* KEYS
. -
تحقق من قيمة المفتاح من خلال الأمر
GET
. - راجع أوامر redis-cli للبحث عن الأمر المناسب لضبط قيمة العداد على 9001.
- تأكد من أن القيمة الجديدة ستعمل عند تحديث الصفحة http://localhost:3000/statistics.
- احذف المفتاح باستخدام واجهة سطر الأوامر وتأكد أن العداد سيعمل عند إضافة مهام جديدة.
الذاكرة المقيمة وقاعدة البيانات Redis
أشرنا سابقًا أنّ قاعدة البيانات Redis لا تحتفظ بالبيانات افتراضيًا، لكنه أمر يمكن حله بسهولة، إذ كل ما علينا فعله هو تشغيل Redis بأمر مختلف كما توضح صفحة Docker hub:
services: redis: # أي شيء آخر command: ['redis-server', '--appendonly', 'yes'] # CMD تعديل volumes: # التصريح عن القرص - ./redis_data:/data
ستقيم البيانات الآن في المجلد على الجهاز المُضيف، وتذكّر إضافة المجلد إلى الملف gitignore.
.
إمكانات أخرى لقاعدة البيانات Redis
تقدم Redis عدّة ميزات إضافةً إلى عمليات إضافة وضبط وحذف المفاتيح، فهي تحدد مثلًا فترة صلاحية المفتاح وهذه ميزة شديدة الأهمية عند استعمالها مثل مخزن مؤقت.
كما يمكن استخدام Redis في إنجاز نماذج نشر-اشتراك publish-subscribe -أواختصارًا PubSub-، وهي آلية تواصل غير متزامنة للتطبيقات الموزّعة. تعمل Redis في هذه الحالة مثل وسيط بين تطبيقين أو أكثر، ينشر بعضها رسائل بإرسالها إلى Redis وعند وصول هذه الرسائل تُبلغ Redis جميع الأطراف بأنها اشتركت بهذه الرسائل.
التمرين 12.12 البيانات المقيمة و Redis
تأكد أن البيانات لا تُخزّن افتراضيًا في قاعدة البيانات Redis بأن تكون قيمة العداد 0 بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down
و docker-compose -f docker-compose.dev.yml up
.
أنشئ بعد ذلك قرصًا للبيانات عن طريق تعديل الملف "todo-app/todo-backend/docker-compose.dev.yml"، وتأكد من بقاء البيانات بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down
وdocker-compose -f docker-compose.dev.yml up
.
ترجمة -وبتصرف- للفصل Building and Configuring Environments من سلسلة Deep Dive Into Modern Web Development
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.