بناء البيئات وتهيئتها
في القسم السابق، استخدمنا صورتين أساسيتين مختلفتين: ubuntu وnode، وقمنا ببعض العمل اليدوي لتشغيل تطبيق بسيط يطبع «Hello, World!». وستكون الأدوات والأوامر التي تعلمناها خلال تلك العملية مفيدة. في هذا القسم، سنتعلم كيفية بناء الصور وتهيئة البيئات لتطبيقاتنا. سنبدأ بواجهة خلفية عادية بـExpress/Node.js ثم نبني فوقها خدمات أخرى، بما في ذلك قاعدة بيانات MongoDB.
Dockerfile
بدلاً من تعديل حاوية عبر نسخ الملفات إليها، يمكننا إنشاء صورة جديدة تحتوي على تطبيق «Hello, World!». الأداة اللازمة لذلك هي Dockerfile. وملف Dockerfile هو ملف نصي بسيط يحتوي على كل التعليمات اللازمة لإنشاء صورة. لننشئ ملف Dockerfile مثالياً لتطبيق «Hello, World!».
لننشئ الآن مجلداً وملفاً باسم Dockerfile داخل ذلك المجلد. ولنضع أيضاً ملف index.js يحتوي على console.log('Hello, World!') بجانب ملف Dockerfile. يبدو هيكل المجلد هكذا:
├── index.js
└── Dockerfile
داخل ملف Dockerfile سنخبر الصورة بثلاثة أمور:
- استخدام node:24 كأساس لصورتنا
- تضمين ملف index.js داخل الصورة، حتى لا نحتاج إلى نسخه يدوياً إلى الحاوية
- عند تشغيل حاوية من الصورة، استخدم Node لتنفيذ ملف index.js.
ستترجم الأمنيات أعلاه إلى ملف Dockerfile أساسي. وأفضل مكان لوضع هذا الملف هو عادةً جذر المشروع.
يبدو Dockerfile الناتج هكذا:
FROM node:24
WORKDIR /usr/src/app
COPY ./index.js ./index.js
CMD ["node", "index.js"]
تُخبر تعليمة FROM برنامج Docker بأن أساس الصورة هو node:24. وتنسخ تعليمة COPY الملف index.js من الجهاز المضيف إلى ملف بالاسم نفسه داخل الصورة. أما تعليمة CMD فتحدد الأمر الافتراضي الذي يُنفَّذ عند تشغيل حاوية باستخدام docker run. نحن نستخدم صيغة exec CMD ["node", "index.js"] التي تنفّذ الملف التنفيذي node مع index.js كوسيط له.
أُضيفت تعليمة WORKDIR لضمان عدم تعارضنا مع محتويات الصورة. فهي تضمن أن يكون /usr/src/app هو مجلد العمل لجميع الأوامر التالية. وإذا لم يكن المجلد موجوداً في الصورة الأساسية، فسيُنشأ تلقائياً.
إذا لم نحدد WORKDIR، فإننا نخاطر بالكتابة فوق ملفات مهمة عن طريق الخطأ. وإذا فحصت الجذر (/) في صورة node:24 باستخدام
docker run node:24 lsفستلاحظ كل المجلدات والملفات المضمّنة أصلاً في الصورة.
يمكننا الآن استخدام الأمر docker build لبناء صورة انطلاقاً من ملف Dockerfile. لنضف إلى الأمر علامة إضافية واحدة: -t التي ستساعدنا على تسمية الصورة:
$ docker build -t fs-hello-world .
[+] Building 3.9s (8/8) FINISHED
...
إذن النتيجة هي
يا Docker، ابنِ بالوسم (يمكنك اعتبار الوسم اسم الصورة الناتجة) fs-hello-world ملفَ Dockerfile الموجود في هذا المجلد.
يمكنك الإشارة إلى أي ملف Dockerfile، لكن في حالتنا تعني النقطة البسيطة أن ملف Dockerfile موجود في هذا المجلد. ولهذا ينتهي الأمر بنقطة. بعد انتهاء البناء، يمكنك تشغيله باستخدام docker run fs-hello-world:
$ docker run fs-hello-world
Hello, World
بما أن الصور مجرد ملفات، فيمكن نقلها وتنزيلها وحذفها. يمكنك سرد الصور الموجودة لديك محلياً باستخدام docker image ls، وحذفها باستخدام docker image rm. ولمعرفة الأوامر الأخرى المتاحة لديك استخدم docker image --help.
أمر آخر: ذُكر أن الأمر الافتراضي المحدد بواسطة CMD في ملف Dockerfile يمكن تجاوزه عند الحاجة. يمكننا مثلاً فتح جلسة bash في الحاوية وفحص محتواها:
$ docker run -it fs-hello-world bash
root@2932e32dbc09:/usr/src/app# ls
index.js
root@2932e32dbc09:/usr/src/app#
صورة أكثر فائدة
نقل خادم Express إلى حاوية ينبغي أن يكون بسيطاً مثل نقل تطبيق «Hello, World!» إلى حاوية. الفرق الوحيد هو وجود ملفات أكثر. ولحسن الحظ، يمكن لتعليمة COPY التعامل مع كل ذلك. لنحذف ملف index.js وننشئ خادم Express جديداً. لنستخدم express-generator لإنشاء هيكل أساسي لتطبيق Express.
$ 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
تتوقع صيغة exec من تعليمة CMD وسائط بالصيغة ["executable", "param1", "param2", ...]، أي أن العنصر الأول يجب أن يكون أمراً تنفيذياً. في حالتنا، يبدأ الأمر بتعيين متغير بيئة، وهذا لا يحقق هذا الشرط. النهج الصحيح هو تعيين متغير البيئة باستخدام تعليمة ENV بدلاً من ذلك.
لنضع ملف Dockerfile التالي في جذر المشروع:
FROM node:24
WORKDIR /usr/src/app
COPY . .
ENV DEBUG=playground:*
CMD ["npm", "start"]
يمكننا الآن بناء الصورة من ملف Dockerfile وتشغيلها:
docker build -t express-server .
docker run -p 3123:3000 express-server
تُخبر العلامة -p في أمر التشغيل برنامج Docker بضرورة فتح منفذ من الجهاز المضيف وتوجيهه إلى منفذ في الحاوية. الصيغة هي -p host-port:application-port.
التطبيق يعمل الآن! لنختبره بإرسال طلب GET إلى http://localhost:3123/.
إذا لم يعمل لديك، فتجاوز إلى القسم التالي. فهناك تفسير لسبب عدم عمله حتى لو اتبعت الخطوات بشكل صحيح.
إيقاف التطبيق يمثل صداعاً في الوقت الحالي. استخدم طرفية أخرى وأمر docker kill لقتل التطبيق. سيرسل الأمر docker kill إشارة قتل (SIGKILL) إلى التطبيق لإجباره على الإيقاف. ويحتاج إلى اسم الحاوية أو معرّفها كوسيط.
لاحظ أنه عند استخدام المعرّف كوسيط، تكفي بداية المعرّف ليعرف Docker أي حاوية نقصد.
$ 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 لحل المشكلة. ملف .dockerignore شبيه جداً بملف .gitignore، ويمكنك استخدامه لمنع نسخ الملفات غير المرغوب فيها إلى صورتك. وينبغي وضع الملف بجانب ملف Dockerfile. إليك محتوى محتملاً لملف .dockerignore
.dockerignore
.gitignore
node_modules
Dockerfile
في حالتنا، لا يُعد ملف .dockerignore الشيء الوحيد المطلوب. نحتاج أيضاً إلى تثبيت الاعتماديات أثناء خطوة البناء. يتغير ملف Dockerfile إلى:
FROM node:24
WORKDIR /usr/src/app
COPY . .
RUN npm install
ENV DEBUG=express:*
CMD ["npm", "start"]
قد يكون npm install محفوفاً بالمخاطر. فبدلاً من استخدام npm install، يقدم npm أداة أفضل بكثير لتثبيت الاعتماديات، وهي أمر ci.
الفروق بين ci وinstall:
- قد يحدّث install ملف package-lock.json
- قد يثبّت install إصداراً مختلفاً من إحدى الاعتماديات إذا كان لديك ^ أو ~ في إصدار الاعتمادية.
- سيحذف ci مجلد node_modules قبل تثبيت أي شيء
- سيتبع ci ملف package-lock.json ولن يغيّر أي ملفات
باختصار: ci ينشئ عمليات بناء موثوقة، بينما install هو الذي تستخدمه عندما تريد تثبيت اعتماديات جديدة.
بما أننا لا نثبّت أي شيء جديد أثناء خطوة البناء، ولا نريد أن تتغير الإصدارات فجأة، فسنستخدم ci:
FROM node:24
WORKDIR /usr/src/app
COPY . .
RUN npm ci
ENV DEBUG=express:*
CMD ["npm", "start"]
والأفضل من ذلك، يمكننا استخدام npm ci --omit=dev لعدم إضاعة الوقت في تثبيت اعتماديات التطوير.
كما لاحظت في المقارنة أعلاه، يحذف
npm ciمجلد node_modules، لذا لم يكن لإنشاء ملف .dockerignore أي أهمية. ومع ذلك، يُعد .dockerignore أداة رائعة عندما تريد تحسين عملية البناء. سنتحدث بإيجاز عن هذه التحسينات لاحقاً.
الآن ينبغي أن يعمل ملف Dockerfile مرة أخرى. جرّبه باستخدام docker build -t express-server . && docker run -p 3123:3000 express-server
لاحظ أننا هنا نسلسل أمرين من أوامر bash باستخدام &&. يمكننا الحصول على التأثير نفسه (تقريباً) بتشغيل الأمرين بشكل منفصل. عند تسلسل الأوامر باستخدام &&، إذا فشل أحد الأوامر فلن تُنفَّذ الأوامر التالية في السلسلة.
أفضل ممارسات Dockerfile
هناك قاعدتان عامتان ينبغي اتباعهما عند إنشاء الصور:
- حاول إنشاء صورة آمنة قدر الإمكان
- حاول إنشاء صورة صغيرة قدر الإمكان
الصور الأصغر أكثر أماناً لأن مساحة الهجوم فيها أقل، كما تنتقل أسرع في خطوط أنابيب النشر.
لدى Snyk قائمة رائعة بأفضل 10 ممارسات لوضع تطبيقات Node/Express في حاويات. اقرأها من هنا.
لا تزال هناك مشكلة مهمة هي أن التطبيق يعمل بصلاحيات root بدلاً من مستخدم أقل امتيازاً. لنُجرِ تغييراً أخيراً على ملف Dockerfile ونستخدم تعليمة USER لتعيين المستخدم إلى غير root.
FROM node:24
WORKDIR /usr/src/app
COPY --chown=node:node . .
RUN npm ci
ENV DEBUG=playground:*
USER node
CMD ["npm", "start"]
5. وضع تطبيق Node في حاوية
استخدام Docker compose
في القسم السابق، أنشأنا خادم Express مع علمنا بأنه سيعمل على المنفذ 3123، واستخدمنا الأمرين docker build -t express-server . && docker run -p 3123:3000 express-server لتشغيله. يبدو هذا بالفعل كشيء ستحتاج إلى وضعه في سكربت لتتذكره. لحسن الحظ، يقدم لنا Docker حلاً أفضل.
Docker compose أداة رائعة أخرى يمكنها مساعدتنا في إدارة الحاويات. لنبدأ باستخدام compose بينما نتعلم المزيد عن الحاويات لأنه سيوفر علينا بعض الوقت في التهيئة.
يمكننا الآن تحويل التعويذة السابقة إلى ملف yaml. وأفضل ما في ملفات yaml هو أنه يمكنك حفظها في مستودع Git!
سننشئ الآن ملف docker-compose.yml ونضعه في جذر المشروع بجانب ملف Dockerfile. هذه المرة، سنستخدم المنفذ نفسه للمضيف والحاوية. محتوى الملف هو:
services:
app: # اسم الخدمة، يمكن أن يكون أي شيء
image: express-server # يحدد الصورة التي سيتم استخدامها
build: . # يحدد مكان البناء إذا لم توجد الصورة
ports: # يحدد المنافذ التي سيتم نشرها
- 3000:3000
شُرح معنى كل سطر في تعليق. وإذا أردت رؤية المواصفات الكاملة فراجع التوثيق.
يمكننا الآن استخدام docker compose up لبناء التطبيق وتشغيله. وإذا أردنا إعادة بناء الصور، يمكننا استخدام docker compose up --build.
يمكنك أيضاً تشغيل التطبيق في الخلفية باستخدام docker compose up -d (حيث -d تعني منفصل) وإغلاقه باستخدام docker compose down.
لاحظ أن بعض إصدارات Docker الأقدم (خاصة في Windows) لا تدعم الأمر docker compose. ومن طرق التحايل على هذه المشكلة تثبيت الأمر المستقل docker-compose الذي يعمل بشكل مشابه في الغالب لـdocker compose. لكن الإصلاح الأفضل هو تحديث Docker إلى إصدار أحدث.
غالباً ما تكون ممارسة رائعة إنشاء ملفات مثل docker-compose.yml تُصرّح بما تريده بدلاً من ملفات سكربت تحتاج إلى تشغيلها بترتيب معين أو عدد معين من المرات.
6. Docker Compose
الاستفادة من الحاويات أثناء التطوير
عند تطوير البرمجيات، يمكن استخدام الحاويات بطرق متنوعة لتحسين جودة حياتك. فباستخدام الحاويات، يمكنك تجنب تهيئة الأدوات عدة مرات، وفي كثير من الحالات يمكنك تخطي تثبيتها على جهازك المضيف تماماً.
يمكنك حتى وضع بيئة التطوير بأكملها في حاوية إذا أردت السير في هذا الاتجاه، وسنعود إلى هذه الفكرة لاحقاً. لكننا الآن سنركّز على تشغيل تطبيق Node داخل حاوية مع إبقاء بقية سير عملك على المضيف.
التطبيق الذي قابلناه في التمارين السابقة يستخدم MongoDB. لنستكشف Docker Hub للعثور على صورة MongoDB. Docker Hub هو المكان الافتراضي الذي يسحب منه Docker الصور، ويمكنك استخدام سجلات أخرى أيضاً، لكن بما أننا غرقنا حتى الرُّكب في Docker فهو خيار جيد. ببحث سريع، يمكننا العثور على https://hub.docker.com/_/mongo
أنشئ ملف yaml جديداً باسم todo-app/todo-backend/docker-compose.dev.yml يبدو كما يلي:
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، وهو دور «المستخدم الخارق».
متغير البيئة الأخير MONGO_INITDB_DATABASE سيخبر MongoDB بإنشاء قاعدة بيانات بهذا الاسم.
يمكنك استخدام العلامة -f لتحديد ملف لتشغيل أمر Docker Compose، وعلينا استخدام العلامة الآن لأن لدينا أكثر من ملف compose. لنشغّل الآن MongoDB:
docker compose -f docker-compose.dev.yml up -d
كما ذُكر سابقاً، لا نريد حالياً تشغيل تطبيق Node داخل حاوية. فالتطوير بينما التطبيق نفسه داخل حاوية يمثل تحدياً. سنستكشف ذلك الخيار لاحقاً في هذا الجزء.
شغّل أولاً الأمر القديم الجيد npm install على جهازك لإعداد تطبيق Node. ثم ابدأ التطبيق بمتغير البيئة المناسب. يمكنك تعديل الشيفرة لتعيينها كقيم افتراضية أو استخدام ملف .env. ولا ضرر في وضع هذه المفاتيح على GitHub لأنها تُستخدم فقط في بيئة التطوير المحلية لديك. سنضعها مع npm run dev لمساعدتك في النسخ واللصق.
MONGO_URL=mongodb://localhost:3456/the_database npm run dev
لن يكون هذا كافياً؛ نحتاج إلى إنشاء مستخدم ليُصرَّح له داخل الحاوية. فالرابط http://localhost:3000/todos يؤدي إلى خطأ في المصادقة:
[nodemon] 3.1.14
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node ./bin/www`
GET /todos 500 10015.695 ms - 519
MongooseError: Operation `todos.find()` buffering timed out after 10000ms
at Timeout._onTimeout (/Users/mluukkai/opetus/2026-fs/osa12/tehtavat/todo-app/todo-backend/node_modules/mongoose/lib/drivers/node-mongodb-native/collection.js:131:25)
at listOnTimeout (node:internal/timers:605:17)
at process.processTimers (node:internal/timers:541:7)
GET /todos 500 12.396 ms - 1808
MongoServerError: Command find requires authentication
at Connection.sendCommand (/Users/mluukkai/opetus/2026-fs/osa12/tehtavat/todo-app/todo-backend/node_modules/mongodb/lib/cmap/connection.js:320:27)
يمكننا استخدام مستخدم المدير الخارق لمصادقة قاعدة البيانات، بحيث يعمل الأمر التالي
MONGO_URL=mongodb://root:example@localhost:3456/the_database?authSource=admin npm run dev
لكن ليس من الجيد استخدام حساب مستخدم خارق للوصول العادي إلى قاعدة البيانات، لذا سنختار حلاً أكثر أماناً.
الربط (bind mount) وتهيئة قاعدة البيانات
في توثيق MongoDB على Docker Hub، يوضح قسم تهيئة نسخة جديدة كيفية تقديم ملفات JavaScript تعمل عند بدء حاوية Mongo. وهذا يتيح لك أتمتة خطوات الإعداد مثل إنشاء المستخدمين أو تهيئة قواعد البيانات.
يحتوي مشروع التمرين على ملف 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 });
سيهيّئ هذا الملف قاعدة البيانات بمستخدم وبعض المهام (todos). بعد ذلك، نحتاج إلى إدخاله إلى الحاوية عند بدء التشغيل.
يمكننا إنشاء صورة جديدة FROM mongo ونسخ الملف إليها، أو يمكننا استخدام ربط bind mount لتثبيت الملف mongo-init.js في الحاوية. لنفعل الخيار الأخير.
الربط (bind mount) هو ربط ملف (أو مجلد) على الجهاز المضيف بملف (أو مجلد) في الحاوية. ويتم الربط بإضافة العلامة -v إلى الأمر container run. الصيغة هي -v FILE-IN-HOST:FILE-IN-CONTAINER.
بما أننا تعلمنا بالفعل عن Docker Compose فلنتخطَّ ذلك. يُصرَّح عن الربط تحت المفتاح volumes في ملف docker-compose.dev.yml. وبخلاف ذلك، الصيغة نفسها: المضيف أولاً ثم الحاوية:
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 لأن منح مزيد من الامتيازات قد يمثل مشكلة أمنية. استخدم chmod فقط على ملف mongo-init.js على جهازك.
الآن ينبغي أن يعمل تشغيل تطبيق Express مع متغير البيئة الصحيح:
MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database npm run dev
لنتحقق من أن http://localhost:3000/todos يعيد المهمتين (todos) اللتين أدخلناهما في التهيئة. يمكننا، بل ينبغي، استخدام Postman لاختبار الوظائف الأساسية للتطبيق، مثل إضافة مهمة أو حذفها.
ما زالت هناك مشكلات؟
لسبب ما، سببت تهيئة Mongo مشكلات لكثيرين.
إذا لم يعمل التطبيق وما زلت تنتهي بالخطأ التالي:
/Users/mluukkai/dev/fs-ci/repo/todo-app/todo-backend/node_modules/mongodb/lib/cmap/connection.js:272
callback(new MongoError(document));
^
MongoError: command find requires authentication
at MessageStream.messageHandler (/Users/mluukkai/dev/fs-ci/repo/todo-app/todo-backend/node_modules/mongodb/lib/cmap/connection.js:272:20)
شغّل هذه الأوامر:
docker compose -f docker-compose.dev.yml down --volumes
docker image rm mongo
بعد ذلك، حاول تشغيل Mongo مرة أخرى.
إذا استمرت المشكلة، فلنتخلَّ عن فكرة وحدة التخزين (volume) تماماً وننسخ سكربت التهيئة إلى صورة مخصصة. أنشئ ملف Dockerfile التالي في المجلد todo-app/todo-backend/mongo:
FROM mongo
COPY ./mongo-init.js /docker-entrypoint-initdb.d/
ابنِه إلى صورة بالأمر:
docker build -t initialized-mongo .
الآن غيّر ملف docker-compose.dev.yml ليستخدم الصورة الجديدة:
mongo:
image: initialized-mongo
ports:
- 3456:27017
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
MONGO_INITDB_DATABASE: the_database
الآن ينبغي أن يعمل التطبيق أخيراً.
حفظ البيانات بشكل دائم باستخدام وحدات التخزين
افتراضياً، لن تحافظ حاويات قواعد البيانات على بياناتنا. وعند إغلاق حاوية قاعدة البيانات قد تتمكن أو قد لا تتمكن من استعادة البيانات.
في الواقع، Mongo حالة نادرة حيث تحافظ الحاوية فعلاً على البيانات. ويحدث هذا لأن المطورين الذين صنعوا صورة Docker لـMongo عرّفوا وحدة تخزين (volume) لاستخدامها. هذا السطر في ملف Dockerfile سيرشد Docker إلى الحفاظ على البيانات في وحدة تخزين.
هناك طريقتان مختلفتان لتخزين البيانات:
- تحديد موقع في نظام ملفاتك (يُسمى ربط bind mount)
- ترك Docker يقرر مكان تخزين البيانات (وحدة تخزين volume)
الخيار الأول مفضل في معظم الحالات كلما احتاج المرء حقاً إلى تجنب حذف البيانات.
لنرَ كليهما عملياً مع Docker compose. لنبدأ بـالربط bind mount:
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. بعد تشغيل التطبيق (docker compose -f docker-compose.dev.yml up) يمكنك سرد وحدات التخزين باستخدام docker volume ls، وفحص إحداها باستخدام docker volume inspect، وحتى حذفها باستخدام docker volume rm:
$ docker volume ls
DRIVER VOLUME NAME
local todo-backend_mongo_data
$ docker volume inspect todo-backend_mongo_data
[
{
"CreatedAt": "2026-03-01T11:48:30Z",
"Driver": "local",
"Labels": {
"com.docker.compose.config-hash": "04a085ca56ca9326ae3e24058b0dc2a710003ba3ca583a2e333b4d6c491cfb94",
"com.docker.compose.project": "todo-backend",
"com.docker.compose.version": "2.33.1",
"com.docker.compose.volume": "mongo_data"
},
"Mountpoint": "/var/lib/docker/volumes/todo-backend_mongo_data/_data",
"Name": "todo-backend_mongo_data",
"Options": null,
"Scope": "local"
}
]
لا تزال وحدة التخزين المسماة مخزنة في نظام ملفاتك المحلي، لكن معرفة مكان تخزينها قد لا يكون بهذه السهولة كما في الخيار السابق.
7. قليل من البرمجة بـMongoDB
تصحيح المشكلات في الحاويات
عند البرمجة، من المرجح أن تنتهي في حالة يكون فيها كل شيء معطلاً.
- Matti Luukkainen
عند التطوير باستخدام الحاويات، نحتاج إلى تعلم أدوات جديدة لتصحيح الأخطاء، إذ لا يمكننا ببساطة «console.log» كل شيء. وعندما تحتوي الشيفرة على خطأ، غالباً ما تكون في حالة يعمل فيها شيء ما على الأقل، فتستطيع التقدم انطلاقاً من ذلك. أما التهيئة فغالباً ما تكون في إحدى حالتين: 1. تعمل أو 2. معطلة. سنستعرض بعض الأدوات التي يمكنها المساعدة عندما يكون تطبيقك في الحالة الثانية.
عند تطوير البرمجيات، يمكنك التقدم بأمان خطوة بخطوة، متحققاً طوال الوقت من أن ما كتبته يتصرف كما هو متوقع. لكن هذا غالباً لا ينطبق على التهيئة. فالتهيئة التي قد تكتبها يمكن أن تكون معطلة حتى اللحظة التي تكتمل فيها. لذا عندما تكتب ملف docker-compose.yml أو Dockerfile طويلاً ولا يعمل، عليك أن تتوقف لحظة وتفكر في الطرق المختلفة التي يمكنك بها التأكد من أن شيئاً ما يعمل.
شكّك في كل شيء لا يزال قابلاً للتطبيق هنا. وكما قيل في الجزء 3: المفتاح هو أن تكون منهجياً. وبما أن المشكلة قد توجد في أي مكان، فعليك أن تشكّ في كل شيء، وتستبعد كل مصادر الخطأ المحتملة واحداً تلو الآخر.
بالنسبة لي، أثمن طريقة لتصحيح الأخطاء هي التوقف والتفكير في ما أحاول إنجازه بدلاً من مجرد خبط رأسي في المشكلة. فغالباً ما يوجد حل بديل بسيط أو بحث سريع على Google يدفعني إلى الأمام.
exec
أمر Docker exec سلاح قوي. ويمكن استخدامه للقفز مباشرة إلى الحاوية عندما تكون قيد التشغيل.
لنبدأ خادم ويب في الخلفية ونقم بقليل من التصحيح لنجعله يعمل ويعرض الرسالة «Hello, exec!» في متصفحنا. لنختر Nginx وهو، بين أمور أخرى، خادم قادر على تقديم ملفات HTML الثابتة. وله ملف index.html افتراضي يمكننا استبداله.
$ docker container run -d nginx
حسناً، الأسئلة الآن هي:
- إلى أين نذهب بمتصفحنا؟
- هل هو حتى قيد التشغيل؟
نعرف كيف نجيب عن السؤال الأخير: بسرد الحاويات قيد التشغيل.
$ docker container ls
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3f831a57b7cc nginx ... 3 sec ago Up 2 sec 80/tcp keen_darwin
أجل! لقد أجبنا عن السؤال الأول أيضاً. يبدو أنه يستمع على المنفذ 80، كما يظهر في المخرجات أعلاه.
لنوقفه ونعيد تشغيله بالعلامة -p ليتسنى لمتصفحنا الوصول إليه.
$ docker container stop keen_darwin
$ docker container rm keen_darwin
$ docker container run -d -p 8080:80 nginx
ملاحظة المحرر: أثناء التطوير، من الضروري متابعة سجلات الحاوية باستمرار. أنا عادةً لا أشغّل الحاويات في الوضع المنفصل (أي مع
-d) لأنه يتطلب جهداً إضافياً بسيطاً لفتح السجلات.عندما أكون متأكداً 100% أن كل شيء يعمل... لا، عندما أكون متأكداً 200%، حينها قد أسترخي قليلاً وأشغّل الحاويات في الوضع المنفصل. إلى أن ينهار كل شيء مرة أخرى ويحين وقت فتح السجلات من جديد.
لننظر إلى التطبيق بالانتقال إلى http://localhost:8080. يبدو أنه يعرض رسالة خاطئة! لنقفز مباشرة إلى الحاوية ونصلح هذا. أبقِ متصفحك مفتوحاً، فلن نحتاج إلى إيقاف الحاوية لإجراء هذا الإصلاح. سننفّذ bash داخل الحاوية، والعلامتان -it تضمنان أننا نستطيع التفاعل مع الحاوية:
$ docker container ls
CONTAINER ID IMAGE COMMAND PORTS NAMES
7edcb36aff08 nginx ... 0.0.0.0:8080->80/tcp wonderful_ramanujan
$ docker exec -it wonderful_ramanujan bash
root@7edcb36aff08:/#
بعد أن دخلنا، نحتاج إلى إيجاد الملف المعطوب واستبداله. يخبرنا بحث سريع على Google أن الملف نفسه هو /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 تماماً كما فعلنا في القسم السابق.
8. واجهة سطر أوامر MongoDB
Redis
Redis هو مخزن مفتاح-قيمة (key-value)، أي أنه يخزّن البيانات كأزواج بسيطة: مفتاح والقيمة المرتبطة به. وخلافاً لقواعد البيانات الموجّهة بالمستندات مثل MongoDB، لا ينظّم Redis البيانات في مجموعات أو جداول. بل يحتفظ بقطع بيانات فردية تسترجعها مباشرةً بالإشارة إلى مفاتيحها.
افتراضياً، يعمل Redis في الذاكرة (in-memory)، ما يعني أنه لا يخزّن البيانات بشكل دائم.
من حالات الاستخدام الممتازة لـRedis استخدامه كذاكرة مؤقتة (cache). فغالباً ما تُستخدم الذاكرات المؤقتة لتخزين بيانات يكون جلبها بطيئاً بخلاف ذلك، وحفظها حتى تصبح غير صالحة. وبعد أن تصبح الذاكرة المؤقتة غير صالحة، تجلب البيانات مرة أخرى وتخزّنها في الذاكرة المؤقتة.
لا علاقة لـRedis بالحاويات. لكن بما أننا قادرون بالفعل على إضافة أي خدمة طرف ثالث إلى تطبيقاتك، فلمَ لا نتعرف على خدمة جديدة؟
9. إعداد Redis للمشروع
10. استعد للانطلاق!
11. واجهة سطر أوامر Redis
حفظ بيانات Redis بشكل دائم
ذُكر في القسم السابق أن Redis افتراضياً لا يحفظ البيانات بشكل دائم. لكن تفعيل الحفظ الدائم سهل. نحتاج فقط إلى تشغيل Redis بأمر مختلف، كما هو موضح في صفحة Docker Hub:
services:
redis:
# كل ما تبقى
command: ['redis-server', '--appendonly', 'yes'] # الكتابة فوق CMD
volumes: # التصريح عن وحدة التخزين
- ./redis_data:/data
ستُحفظ البيانات الآن بشكل دائم في المجلد redis_data على الجهاز المضيف. تذكر إضافة المجلد إلى .gitignore!
وظائف أخرى لـRedis
بالإضافة إلى عمليات GET وSET وDEL على المفاتيح والقيم، يمكن لـRedis فعل الكثير أيضاً. فيمكنه مثلاً إنهاء صلاحية المفاتيح تلقائياً، وهي ميزة مفيدة جداً عند استخدام Redis كذاكرة مؤقتة.
يمكن أيضاً استخدام Redis لتنفيذ ما يُسمى نمط النشر-الاشتراك (publish-subscribe) (أو PubSub) وهو آلية اتصال غير متزامنة للبرمجيات الموزعة. في هذا السيناريو، يعمل Redis كـوسيط رسائل بين خدمتين أو أكثر. بعض الخدمات تنشر رسائل بإرسالها إلى Redis، والذي يخبر عند وصول رسالة الأطراف التي اشتركت في تلك الرسائل.
12. حفظ البيانات بشكل دائم في Redis