مشاركة الملفات مع Docker باستخدام Bind Mounts
السلام عليكم ورحمة الله وبركاته
يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:
المقدمة
في المقالة السابقة تعرفنا على مشكلة فقدان البيانات عند حذف الـ Container
وتعلمنا كيف نستخدم الـ Volumes لحفظ البيانات بشكل دائم خارج الـ Container
في هذه المقالة سنتعرف على طريقة أخرى لحفظ البيانات وهي الـ Bind Mounts
هى مثلها مثل الـ Volumes لكن تختلف في طريقة الربط وإدارة البيانات، حيث أن الـ Bind Mounts تتيح لك ربط مجلد معين من جهازك مباشرة بمجلد داخل الـ Container
أما الـ Volumes فيقوم الـ Docker بإنشاء مساحة تخزين خاصة به مستقلة ويقوم الـ Docker بإدارتها بنفسه
ما هو الـ Bind Mount ؟
الـ Bind Mount ببساطة هو أنك تربط مجلد معين من جهازك بمجلد داخل الـ Container
أي تغيير في أحدهما ينعكس مباشرة على الآخر
الفرق الجوهري بينه وبين الـ Volume هو أنه في الـ Volume الـ Docker يختار مكان التخزين بنفسه ويديره بنفسه
أما في الـ Bind Mount أنت من يحدد المكان بالضبط على جهازك
حذف الـ Images و Containers و Volumes السابقة
سنستخدم نفس المشروع الذي بنيناه في المقالة السابقة وهو تطبيق Node.js بسيط يخزن بيانات المنتجات في ملف products.json
لكن قبل أي شيء، لنقم بحذف كل الـ Images و Containers و Volumes التي أنشأناها في المقالة السابقة
لنرى ما لدينا حاليًا:
> docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
nodejs-docker-app v2.0 a3528ead56a5 2 hours ago 248MB
nodejs-docker-app v3.0 89602c0f6234 2 hours ago 248MB
nodejs-docker-app v1.0 46f7afae6a25 19 hours ago 248MB
لاحظ أننا في المقالة السابقة عند بناء الـ Image في كل مرة نغير الـ tag من v1.0 ثم v2.0 ثم v3.0، هذا لكي أوضح لك للمرة الثالثة أو الرابعة أن الـ tag هو مجرد اسم يمكنك تغييره كيفما تشاء، ولا يعني أي شيء محدد، هو فقط لتوضيح أن هذا إصدار جديد من الـ Image بعد التعديلات التي قمنا بها
ويمكنك أن تضع أي اسم تريده، مثلاً latest أو myapp أو حتى abc123، المهم أن يكون لديك طريقة لتتبع إصدارات الـ Image الخاصة بك
لنقم بحذف كل هذه الـ Images:
> docker image remove nodejs-docker-app:v1.0 nodejs-docker-app:v2.0 nodejs-docker-app:v3.0
Untagged: nodejs-docker-app:v1.0
Deleted: sha256:46f7afae6a25e884619bfca1e86bc06885766f2d5ec53020413dc54cf20d8dfb
Untagged: nodejs-docker-app:v2.0
Deleted: sha256:a3528ead56a5c70dd29d530d73c2e7884c6b6040cb62e629e84c05e23d361e61
Untagged: nodejs-docker-app:v3.0
Deleted: sha256:89602c0f62345f5a5af71b958e1069a3778a47bc4ecc901ab35c3fd803da9f2d
الآن بالتبعية لن نجد أي Container لأن كل الـ Containers التي أنشأناها كانت تعتمد على هذه الـ Images
الآن لنقم بحذف الـ Volume الذي أنشأناه في المقالة السابقة والذي كان اسمه products-data:
> docker volume remove products-data
products-data
تجهيز المشروع والـ Dockerfile
الآن لنبدأ من جديد ونقم بإنشاء Dockerfile جديد للمثال الذي سنستخدمه في هذه المقالة
ملف الـ app.js سيكون كالتالي:
const express = require("express");
const fs = require("fs");
const app = express();
const port = process.env.PORT || 3000;
const dataFilePath = "products.json";
app.use(express.json());
app.get("/products", (req, res) => {
fs.readFile(dataFilePath, "utf-8", (err, data) => {
if (err) {
return res.json([]);
}
res.json(JSON.parse(data));
});
});
app.post("/products", (req, res) => {
const newProduct = req.body;
fs.readFile(dataFilePath, "utf-8", (err, data) => {
const products = err ? [] : JSON.parse(data);
products.push(newProduct);
fs.writeFile(dataFilePath, JSON.stringify(products), (err) => {
if (err) {
return res.status(500).json({ message: "Error saving product" });
}
res.status(201).json(newProduct);
});
});
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
لاحظ أننا لم نعد نستقبل DATA_DIR كـ Environment Variable، بل قمنا بتحديد مسار ثابت للملف products.json في المتغير dataFilePath
لكي نبسط الأمور ونركز فقط على موضوع الـ Bind Mounts فقط، لأن هذه الطريقة تناسب الـ Bind Mounts أكثر من الـ Volumes، حيث أن الـ Bind Mounts تعتمد على مسار ثابت تحدده بنفسك
وأما ملف الـ Dockerfile سنقوم فقط بحذف سطر الـ VOLUME الذي كان يحدد مجلد /app/db كـ Volume
وقمنا بحذف أمر RUN mkdir -p /app/db لأننا لا نريد إنشاء مجلد داخل الـ Container لأننا لن نستخدم Volume
FROM node:25-alpine
WORKDIR /app
COPY package*.json .
RUN npm install
COPY . .
ENV PORT=3000
EXPOSE 3000
ENTRYPOINT ["npm", "start"]
الآن قبل أن نبني الـ Image وننشيء الـ Container، يجب أن ننشئ الملف products.json أولًا على جهازنا
لأن الـ Bind Mount عندما نستخدمه لنربط ملف فهو يشرط أن يكون الملف موجود بالفعل على جهازنا قبل تشغيل الـ Container
إذا لم يكن الملف موجودًا، الـ Docker سينشئ مجلد باسم الملف الذي تريد ربطه، وهذا سيؤدي إلى مشاكل لأننا نريد ربط ملف وليس مجلد
ملحوظة: هذه القاعدة تنطبق فقط على ربط الملفات، أما ربط المجلدات فالـDockerسينشئ المجلد تلقائيًا إذا لم يكن موجودًا
لذا سننشئ ملف products.json داخل مجلد المشروع ونجعله فارغًا:
[]
لنرى ملفات المشروع الآن:
> ls
Dockerfile app.js package.json package-lock.json products.json
الآن لنبني الـ Image:
> docker image build -t nodejs-docker-app:v1.0 .
...
=> [1/5] FROM docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997 0.0s
=> => resolve docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 1.04kB 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package*.json . 0.0s
=> CACHED [4/5] RUN npm install 0.0s
=> [5/5] COPY . . 0.0s
...
الآن كل شيء جاهز
استخدام Bind Mount مع تطبيقنا
تذكر أن في حالة الـ Volume كنا نربط مجلد داخل الـ Container بالـ Volume
والبيانات لا تحتفظ ولا تخزن في مجلد المشروع الأساسي على جهازك، بل في الـ Volume الذي يديره الـ Docker بنفسه
أما في حالة الـ Bind Mount، فلا يتم إنشاء أي Volume، بل أنت تحدد مجلد معين من جهازك وتربطه مباشرة بمجلد داخل الـ Container
أي تغيير في هذا المجلد على جهازك ينعكس داخل الـ Container والعكس صحيح، أي تغيير داخل الـ Container ينعكس على جهازك
وفي حالتنا نحن اخترنا أن نربط ملف products.json الموجود في مجلد المشروع على جهازنا بالملف products.json داخل الـ Container في المسار /app/products.json
وطريقة الربط مثلها مثل الـ Volume، لكن بدلاً من كتابة اسم الـ Volume نكتب مسار المجلد أو الملف الذي نريد ربطه
بهذا الشكل -v ${PWD}/products.json:/app/products.json
هكذا نربط ملف products.json الموجود في مجلد المشروع ${PWD}/products.json بملف products.json داخل الـ Container في المسار /app/products.json
ونستخدم ${PWD} لأن الـ Bind Mount يحتاج إلى الـ Absolute Path الكامل للمجلد أو الملف الذي نريد ربطه
و${PWD} هو اختصار لـ Print Working Directory أي المسار الكامل للمجلد الحالي الذي نحن فيه، وهو مجلد المشروع
الآن لننشئ الـ Container ونرى الأمر كاملًا:
> docker container run -d --rm --name nodejs-container -p 3000:3000 -v ${PWD}/products.json:/app/products.json nodejs-docker-app:v1.0
5db8966f141c2b248cba16891317ec4d308a64907b7f5976dc74c11d410e02b9
الآن أريدك أن تراقب معي، أي تغيير في الملف products.json على جهازك سينعكس مباشرة داخل الـ Container والعكس صحيح، أي تغيير داخل الـ Container في الملف products.json سينعكس مباشرة على جهازك
لذا لنقم بإضافة منتجات:
> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Keyboard", "price": 100}'
{"name":"Keyboard","price":100}
> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Mouse", "price": 50}'
{"name":"Mouse","price":50}
الآن لنقم بالتأكد فقط من أن GET /products يعرض المنتجات التي أضفناها:
> curl http://localhost:3000/products
[{"name":"Keyboard","price":100},{"name":"Mouse","price":50}]
الآن لنرى الملف على جهازنا:
> cat products.json
[{"name":"Keyboard","price":100},{"name":"Mouse","price":50}]
لاحظ أن التغييرات التي قمنا بها داخل الـ Container في الملف products.json انعكست مباشرة على الملف products.json الموجود على جهازنا
والعكس صحيح أيضًا، لو عدلنا الملف من جهازنا:
[
{ "name": "Keyboard", "price": 100 },
{ "name": "Mouse", "price": 50 },
{ "name": "Monitor", "price": 300 }
]
لقد قمنا بإضافة منتج جديد في الملف products.json على جهازنا، الآن إذا قمنا بعمل GET /products داخل الـ Container سنرى هذا المنتج الجديد:
> curl http://localhost:3000/products
[{"name":"Keyboard","price":100},{"name":"Mouse","price":50},{"name":"Monitor","price":300}]
لاحظ أن التغير الذي قمنا به على جهازنا في الملف products.json انعكس مباشرة داخل الـ Container في نفس الملف products.json
وطبعًا حتى لو حذفنا الـ Container، الملف products.json سيبقى موجودًا على جهازنا ولن يتم حذفه، لأن البيانات لا تخزن داخل الـ Container بل لا تخزن في Docker على الإطلاق، بل تبقى كما هي على جهازنا
و Docker لا يملك أي سيطرة على هذا الملف، هو فقط يربطه داخل الـ Container
لنقم بإيقاف الـ Container:
> docker container stop nodejs-container
nodejs-container
هكذا تم إيقاف الـ Container وحذفه تلقائيًا بسبب خيار --rm
الآن ستجد أن الملف products.json لا يزال موجودًا على جهازنا وبنفس البيانات التي أضفناها
الآن لنقم بإنشاء Container جديد ونربطه بنفس الملف products.json:
> docker container run -d --rm --name nodejs-container -p 3000:3000 -v ${PWD}/products.json:/app/products.json nodejs-docker-app:v1.0
0372c9e7f52c5b4f25727f6b70187a17e625469f2476a3c7d7e4aa890c85a19d
الآن إذا قمنا بعمل GET /products داخل الـ Container سنرى نفس البيانات الموجودة في الملف products.json على جهازنا، لأن الـ Bind Mount يربط نفس الملف داخل الـ Container:
> curl http://localhost:3000/products
[{"name":"Keyboard","price":100},{"name":"Mouse","price":50},{"name":"Monitor","price":300}]
حسنًا كما رأينا، الـ Bind Mount يتيح لنا مشاركة الملفات بين جهازنا والـ Container بشكل مباشر
الآن لنقم بإيقاف الـ Container:
> docker container stop nodejs-container
nodejs-container
متى نستخدم Bind Mounts
أولًا استخدامات الـ Bind Mounts تختلف عن استخدامات الـ Volumes
بحيث أن الـ Volumes يستخدم للتخزين البيانات بشكل دائم خارج الـ Container، والبيانات تخزن في مكان خاص يديره الـ Docker بشكل منعزل
بالتالي ستجده يستخدم مع الـ Database Containers أو أي Container يحتاج إلى تخزين بيانات بشكل عام
أما الـ Bind Mounts لا يستخدم للتخزين
بل يستخدم لربط ملفات معينة من جهازك داخل الـ Container بشكل مؤقت
على سبيل المثال ربط ملفات الـ Configuration
أيضًا يستخدم الـ Bind Mounts لربط ملفات الـ Logs بحيث يمكنك رؤية الـ Logs مباشرة على جهازك بدون الحاجة إلى الدخول إلى الـ Container
أو يستخدم لربط المشروع بأكمله داخل الـ Container أثناء التطوير
بحيث يمكنك تعديل الملفات من جهازك ورؤية التغييرات مباشرة داخل الـ Container
بدون الحاجة إلى إعادة بناء الـ Image في كل مرة بهذا الشكل -v ${PWD}:/app
الخلاصة
ستلاحظ أن استخدامات الـ Bind Mounts قد تنحصر في أمور تساعدك أثناء التطوير أكثر
ولا تستخدم غاليبًا في الأمور المتعلقة بالـ Production أو في الأمور التي تحتاج إلى تخزين بيانات بشكل دائم
لأن الـ Bind Mounts لم تصمم للتخزين الدائم بل لربط الملفات بينك وبين الـ Container بشكل مؤقت
أما الـ Volumes فهي مصممة للتخزين وتستخدم في كل الحالات التي تحتاج فيها إلى تخزين بيانات بشكل دائم خارج الـ Container
على أي حال طالما أنك فهمت الفرق بينهما ومتى تستخدم كل منهما، فهذا هو الأهم
يمكنك استخدام ما تريد بحسب احتياجاتك
أرجو أن تكون قد استفدت من هذا الشرح العملي للـ Bind Mounts