انتقال للمقال
وقت القراءة: ≈ 10 دقائق

مشاركة الملفات مع 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