انتقال للمقال
وقت القراءة: ≈ 25 دقيقة (بمعدل فنجان واحد من القهوة 😊)

حفظ البيانات في Docker باستخدام Volumes

السلام عليكم ورحمة الله وبركاته


المقدمة

في المقالات السابقة تعرفنا على الـ Docker وكيفية إنشاء Containers وإنشاءها
لكن هناك مشكلة مهمة لم نتطرق لها بعد: ماذا يحدث للبيانات عند حذف الـ Container ؟

الإجابة البسيطة هي أن كل شيء داخل الـ Container يختفي عند حذفه
وهذا بديهي جدًا لأن الـ Container مصمم ليكون معزولًا كما كلنا
فأي شيء داخل الـ Container هو مؤقت ويختفي عند حذفه

هذه قد تكون ميزة وتوفر لنا مبدأ الـ Isolation و Statelessness و Ephemerality و Immutability وكل هذه المعاني الرنانة التي يتغنى بها الأشخاص

لكن في نفس الوقت، هذه قد تعد مشكلة خاصة مع قواعد البيانات أو أي تطبيق يحتاج لحفظ بيانات بشكل دائم
فعلى سبيل المثال، إذا كان لدينا تطبيق ويب يستخدم قاعدة بيانات MySQL داخل Container
ثم حذفنا هذا الـ Container، هنا نحن سنفقد كل البيانات التي كانت في قاعدة البيانات

لكن بالطبع الـ Docker يوفر لنا حلولًا لهذه المشكلة من خلال طرق مختلفة مثل الـ Volumes و Bind Mounts
في هذه المقالة سنركز على الـ Volumes وكيفية استخدامها لحفظ البيانات بشكل دائم
وفي المقالة القادمة سنتعرف على الـ Bind Mounts

حذف كل الـ Image و الـ Container الغير مستخدمة

لنرى ما الـ Images التي لدينا حاليًا:

> docker image list
REPOSITORY          TAG       IMAGE ID       CREATED       SIZE
nodejs-docker-app   latest    0d131c009503   6 days ago    248MB
static-site-img     latest    0c34822e06c6   10 days ago   92.2MB
nginx               alpine    b0f7830b6bfa   3 weeks ago   92.2MB
ubuntu              latest    cd1dba651b30   3 weeks ago   117MB
nginx               latest    c881927c4077   3 weeks ago   237MB
alpine              latest    865b95f46d98   7 weeks ago   13MB

لنرى الـ Containers التي لدينا:

> docker container list -a
CONTAINER ID   IMAGE          COMMAND                  CREATED      STATUS                  PORTS                  NAMES
6faf1d1c88e6   0d2758feffac   "npm start ls"           8 days ago   Exited (1) 8 days ago                          nodejs-container

لدينا الكثير من الـ Images التى قمنا بإنشائها في المقالات السابقة والتى لمنعد نستخدمها الآن، لذلك سنقوم بتنفيذ بعض الأوامر التي تساعدنا في مغرفة حجمواستهلاك كل Image و Container لدينا

ومن هذه الأوامر هو أمر docker system df الذي يعرض لنا حجم الـ Images و Containers و Volumes و Build Cache التي لدينا:

> docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          6         0         5.414GB   5.414GB (100%)
Containers      1         0         61.44kB   61.44kB (100%)
Local Volumes   0         0         0B        0B
Build Cache     127       0         6.423GB   6.423GB

لاحظ أن لدينا 6 Images بحجم إجمالي 5.414GB
و Container واحد بحجم 61.44kB
ولا يوجد Volumes حتى الآن
لكن لاحظ وجود Build Cache بحجم 6.423GB وهذا كما يوحى الاسم فهو مساحة الـ cache التي يستخدمها الـ Docker أثناء بناء الـ Images

لدينا بعض الأوامر الأخرى التي تساعدنا في تنظيف الـ Docker وحذف الـ Images و Containers و Volumes الغير مستخدمة، مثل:

  • docker image prune لحذف الـ Images المعلقة التي لا تملك اسم أو Tag
  • docker image prune --all لحذف كل الـ Images الغير مستخدمة أو التي لا تملك اسم أو Tag
  • docker container prune لحذف الـ Containers المتوقفة
  • docker volume prune لحذف الـ Volumes التي لا تملك اسم
  • docker volume prune --all لحذف كل الـ Volumes الغير مستخدمة أو التي لا تملك اسم
  • docker builder prune لحذف الـ Build Cache
  • docker builder prune --all لحذف كل الـ Build Cache الخاصة بكل الـ Images
  • docker system prune لحذف كل ما سبق لكن بدون --all
  • docker system prune --all لحذف كل ما سبق مع --all ليشمل كل المتوقف والمعلق والغير مستخدم والتي لا تملك اسم أو Tag

ملحوظة: عليك توخي الحذر عند استخدام أي من هذه الأوامر لأنها قد تحذف Images أو Containers أو Volumes أو Build Cache مهمة لدينا وتحتاجها، لذلك تأكد دائمًا من مراجعة ما تريد حذفه قبل تنفيذ الأمر

نحن بكل تواضح سننفذ docker system prune --all --volumes --force ليقوم بحذف كل شيء غير مستخدم أو متوقف لدينا

> docker system prune --all --volumes --force
Deleted Containers:
6faf1d1c88e69fc1870230b79eb1ad3cf3df7ac7b96dcbcbc8f0ee667215c009

Deleted Images:
untagged: nodejs-docker-app:latest
deleted: sha256:0d131c00950325336f5a88dae7d9a05016263281d757bbfe6a2ff353589fc83f
...
untagged: static-site-img:latest
deleted: sha256:0c34822e06c63302a00218e069fd7ccccd65072b0f03f32375b6436cfc8cd08d
...
untagged: ubuntu:latest
deleted: sha256:cd1dba651b3080c3686ecf4e3c4220f026b521fb76978881737d24f200828b2b
...
untagged: alpine:latest
deleted: sha256:865b95f46d98cf867a156fe4a135ad3fe50d2056aa3f25ed31662dff6da4eb62
...
untagged: nginx:alpine
untagged: nginx:latest
deleted: sha256:c881927c4077710ac4b1da63b83aa163937fb47457950c267d92f7e4dedf4aec
...

Deleted build cache objects:
zd88yaszjzffvniu39m4q24s5
...

Total reclaimed space: 6.79GB

هنا قمنا بحذف كل الـ Containers المتوقفة وكل الـ Images الغير مستخدمة وكل الـ Build Cache وكل الـ Volumes الغير مستخدمة
الآن لنرى الـ Images التي لدينا:

> docker image list
REPOSITORY          TAG       IMAGE ID       CREATED       SIZE

لا يوجد أي Image لدينا وبالتبعية طالما لا يوجد Image فلا يوجد Container أيضًا

لنرى أمر docker system df الآن:

> docker system df
docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          0         0         0B        0B
Containers      0         0         0B        0B
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

تم حذف كل شيء كأننا بدأنا من الصفر

تجهيز المشروع والـ Dockerfile

سنقوم مشروع بسيط كما في المقالة السابقة وهو عبارة عن تطبيق Node.js يقوم بتخزين بيانات المنتجات في ملف products.json
لنقم بإنشاء مجلد جديد للمشروع:

> mkdir nodejs-docker-data
> cd nodejs-docker-data

> npm init -y
> npm install express

ملف package.json سيكون كالتالي:

{
  "name": "nodejs-docker-data",
  "version": "1.0.0",
  "description": "A simple Node.js application to demonstrate Docker volumes and bind mounts",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^5.2.1"
  }
}

الآن لننشئ ملف 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}`);
});

هذا التطبيق بسيط لديه GET /products لعرض المنتجات و POST /products لإضافة منتج جديد
ونحن نستخدم ملف products.json لتخزين البيانات، بدون Database لتبسيط الأمور فقط

فلنرى ملفات المشروع:

> ls
app.js  package-lock.json  package.json

ملف الـ products.json غير موجود حتى الآن وسيتم إنشاؤه تلقائيًا عند إضافة أول منتج لأنه غير موجود ضمن ملفات المشروع
بمعنى آخر سيتم إنشاءه داخل الـ Container

الآن لننشئ Dockerfile:

FROM node:25-alpine

WORKDIR /app

COPY package*.json .

RUN npm install

COPY . .

ENV PORT=3000
EXPOSE 3000

ENTRYPOINT ["npm", "start"]

ولدينا بالطبع ملف الـ .dockerignore:

node_modules
Dockerfile
.env
.git
.gitignore

الآن كل شيء جاهز لبناء الـ Image وإنشاء الـ Container

> docker image build -t nodejs-docker-app:v1.0 .
...
 => [1/5] FROM docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                               32.0s
 => => resolve docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                                0.0s
 => => sha256:e40759f21733438f9c0a46f29db552e5a38958b5391a61b918f24202b0dac7f8 445B / 445B                                                                                             0.6s
 => => sha256:884cfe835c5d8808d93c5f47b92d898326fd1da708c6953c8ec395ee30ba50c4 1.26MB / 1.26MB                                                                                         2.7s
 => => sha256:589002ba0eaed121a1dbf42f6648f29e5be55d5c8a6ee0f8eaa0285cc21ac153 3.86MB / 3.86MB                                                                                         3.9s
 => => sha256:c54a4b4e2512969ee82b2eff4a00f872663f4a32b3af1bcdad2b72518fc575c9 54.27MB / 54.27MB                                                                                      30.2s
 => => extracting sha256:589002ba0eaed121a1dbf42f6648f29e5be55d5c8a6ee0f8eaa0285cc21ac153                                                                                              0.2s
 => => extracting sha256:c54a4b4e2512969ee82b2eff4a00f872663f4a32b3af1bcdad2b72518fc575c9                                                                                              1.6s
 => => extracting sha256:884cfe835c5d8808d93c5f47b92d898326fd1da708c6953c8ec395ee30ba50c4                                                                                              0.0s
 => => extracting sha256:e40759f21733438f9c0a46f29db552e5a38958b5391a61b918f24202b0dac7f8                                                                                              0.0s
 => [internal] load build context                                                                                                                                                      0.1s
 => => transferring context: 30.36kB                                                                                                                                                   0.0s
 => [2/5] WORKDIR /app                                                                                                                                                                 0.1s
 => [3/5] COPY package*.json .                                                                                                                                                         0.0s
 => [4/5] RUN npm install                                                                                                                                                              2.5s
 => [5/5] COPY . .                                                                                                                                                                     0.1s
...

لاحظ أنه لا يوجد cached في أي خطوة من خطوات بناء الـ Image لأننا قمنا بحذف كل شيء من الـ Docker

الآن لنشغل الـ Container:

> docker container run -d --rm --name nodejs-container -p 3000:3000 nodejs-docker-app:v1.0
dd849112efbb9834f9f4cf88cabdf1b939f03ac5a0ad6cef2bc3a93add5a65db

الآن التطبيق يعمل على http://localhost:3000

لنرى الـ Containers التي تعمل حاليًا:

> docker container list
CONTAINER ID   IMAGE                    COMMAND       CREATED          STATUS          PORTS                    NAMES
dd849112efbb   nodejs-docker-app:v1.0   "npm start"   13 seconds ago   Up 12 seconds   0.0.0.0:3000->3000/tcp   nodejs-container

الآن التطبيق يعمل داخل الـ Container بنجاح
لنرى المشكلة التى تحدثنا عنها سابقًا ونرى كيف يمكننا حلها باستخدام Volumes و Bind Mounts

تجربة تخزين البيانات داخل الـ Container

أولًا قبل أي شيء يجب أن نرى المشكلة بأعيننا
لذا لنقم بإضافة بعض المنتجات لتطبيقنا عن طريق عمل POST /products
سنقوم باستخدام curl لفعل ذلك وأنت يمكننا تستخدم أي أداة أخرى مثل Postman أو Insomnia أو أي وسسيلة تستطيع من خلالها إرسال طلبات HTTP إلى endpoint

نحن هنا سنستخدم curl لكي نضيف منتجين:

> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 1500}'
{"name":"Laptop","price":1500}

> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Phone", "price": 800}'
{"name":"Phone","price":800}

الآن لنتأكد أن المنتجات موجودة عن طريق عمل GET /products:

> curl http://localhost:3000/products
[{"name":"Laptop","price":1500},{"name":"Phone","price":800}]

لاحظ أن المنتجات تم إضافتها بنجاح في ملف products.json داخل الـ Container
لكن المشكلة أنها مخزنة داخل الـ Container نفسه
بالتالي إذا قمنا بحذف هذا الـ Container سنفقد كل هذه البيانات

لذا لنوقف الـ Container الحالي وطالما أننا استخدمنا --rm عند إنشاءه، فسيتم حذف الـ Container تلقائيًا عند الإيقاف:

> docker container stop nodejs-container
nodejs-container

الآن تم إيقاف وحذف الـ Container، لنرى الـ Containers التي لدينا:

> docker container list -a
CONTAINER ID   IMAGE          COMMAND                  CREATED      STATUS                  PORTS                  NAMES

لا يوجد أي Container
الآن لنعيد إنشاء الـ Container من نفس الـ Image:

> docker container run -d --rm --name nodejs-container -p 3000:3000 nodejs-docker-app:v1.0
8aafc85ec48b34e0b94582818d9e6028775f3bc9398c15640b48e6b94f5dc7f3

لنحاول جلب المنتجات التي أضفناها سابقًا:

> curl http://localhost:3000/products
[]

لا شيء، كل المنتجات التي أضفناها لم يعد لها وجود
وهذا هو المتوقع لأن ملف products.json كان داخل الـ Container وطالما الـ Container تم حذفه، فكل شيء داخله تم حذفه أيضًا بالتبعية

تخيل لو هذا كان تطبيق حقيقي بقاعدة بيانات فيها بيانات مستخدمين، هكذا إذا حدث أي شيء خاطيء لـ Container قد نفقد كل البيانات
بالتالي فتخزين البيانات داخل الـ Container ليس خيارًا جيدًا في معظم الحالات
لذلك نحتاج طريقة لحفظ البيانات خارج الـ Container حتى لا تضيع عند حذفه

لنوقف الـ Container الحالي أولًا:

> docker container stop nodejs-container

Docker Volumes

الـ Volume هو مساحة تخزين يديرها الـ Docker بنفسه
الفكرة ببساطة أن الـ Docker ينشئ مكان منفصل على جهازك لتخزين البيانات
وهذا المكان لا يتأثر بحذف أو إعادة إنشاء الـ Container
أنت لا تحتاج حتى لمعرفة أين بالضبط يخزن الـ Docker هذه البيانات، هو يتولى كل شيء
بالتالي أنت تنشيء مساحة تخزين مستقلة عن الـ Container وتربطها بالـ Container عند إنشائه
وإذا تم حذف الـ Container، البيانات في الـ Volume ستبقى سليمة ويمكنك ربطها بأي Container آخر في المستقبل

إنشاء Volume

هناك عدة طرق لإنشاء الـ Volume سواء يدويًا باستخدام أمر docker volume create أو تلقائيًا عند إنشاء الـ Container باستخدام -v أو من خلال تعريفه في الـ Dockerfile باستخدام أمر VOLUME

حاليًا سنقوم بإنشاء Volume يدويًا باستخدام أمر docker volume create:

> docker volume create products-data
products-data

بعد تنفيذ هذا الأمر، تم إنشاء Volume جديد اسمه products-data
ولرؤية جميع الـ Volumes نستخدم أمر docker volume list:

> docker volume list
DRIVER    VOLUME NAME
local     products-data

هنا نرى أن لدينا Volume واحد اسمه products-data وهو من نوع local وهو النوع الافتراضي
ومعناه أن الـ Volume هذا يخزن البيانات على جهازك الشخصي

ولرؤية تفاصيل أكثر عن هذا الـ Volume نستخدم أمر docker volume inspect متبوعًا باسم الـ Volume:

> docker volume inspect products-data
[
    {
        "CreatedAt": "2026-02-06T14:54:56Z",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/products-data/_data",
        "Name": "products-data",
        "Options": null,
        "Scope": "local"
    }
]

هنا نرى أن الـ Volume تم إنشاؤه في المسار /var/lib/docker/volumes/products-data/_data على جهازك
وكل البيانات التي تكتب في هذا الـ Volume ستخزن في هذا المسار
و docker هو الذي يدير هذا الـ Volume ويكون هذا الـ volume مستقل تمامًا عن أي Container يستخدمه
ويمكننا ربطه بأكثر من Container في نفس الوقت إذا أردنا، وكل Container سيرى نفس البيانات في هذا الـ Volume
وبالتالي فإن الـ Volume قد يكون مركز تخزين مشترك بين عدة Containers إذا ربطناهم بنفس الـ Volume

ربط Container بالـ Volume

الآن لننشيء الـ Container مرة أخرى ولكن هذه المرة سنربطه بالـ Volume الذي أنشأناه
لكي الربط بين الـ Container والـ Volume يتم عن طريق أنك تربط الـ Volume بمجلد داخل الـ Container
أي تحتاج لمجلد داخل الـ Container ليكون نقطة الربط بين الـ Volume والـ Container
وهذا المجلد سيشارك البيانات بين الـ Volume والـ Container بشكل متزامن

وستلاحظ أن تطبيقنا الحالي يقوم بإنشاء ملف products.json في الـ WORKDIR الذي حددناه في الـ Dockerfile
لذا نحتاج لإنشاء مجلد داخل الـ Container وليكن /app/db ونربط الـ Volume بهذا المجلد
بحيث أن /app هو الـ WORKDIR الذي حددناه في الـ Dockerfile، وداخل هذا المجلد سننشئ مجلد جديد اسمه db ليكون نقطة الربط مع الـ Volume

أي شيء سيتم تخزينه في هذا المجلد داخل الـ Container سيتم تخزينه في الـ Volume والعكس صحيح، أي أي شيء في الـ Volume سيظهر داخل هذا المجلد في الـ Container

لذا سنقوم بتعديل ملف app.js ليقوم بتخزين البيانات في مجلد /app/db/products.json بدلاً من products.json في الـ WORKDIR بشكل مباشر:

const express = require("express");
const fs = require("fs");
const path = require("path");

const app = express();
const port = process.env.PORT || 3000;
const dataDir = process.env.DATA_DIR || "./db";
const dataFilePath = path.join(dataDir, "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}`);
  console.log(`Data directory: ${dataDir}`);
});

هنا قمنا بإضافة DATA_DIR كـ environment variable يحدد المجلد الذي سيتم تخزين البيانات فيه
وقمنا بإنشاء متغير يدعى dataFilePath الذي يحدد مسار الملف products.json داخل المجلد المحدد في DATA_DIR
هكذا يمكننا تحديد المجلد الذي نريد تخزين البيانات فيه عن طريق تغيير قيمة DATA_DIR عند إنشاء الـ Container

وطالما أن الـ DATA_DIR هو environment variable، فهذا يعني أنه يمكننا تحديده عند إنشاء الـ Container
أو يمكننا تحديده في الـ Dockerfile كقيمة افتراضية على مستوى الـ Image

لكنك ليس مضطرًا لجعل DATA_DIR كـ environment variable، بل يمكننك ببساطة تحديد مسار ثابت داخل الـ Container مثل /app/db/products.json
لكن جعلها كـ environment variable يعطيك مرونة أكثر في تحديد مكان تخزين فقط
أمر يرجع إليك هل تريدها قيمة ثابتة أو هل تريد أن تتحكم فيها عن طريق الـ environment variable

على أي حال، لقد قمنا بجعلها environment variable، لذا سنقوم بتعديل الـ Dockerfile لتحديد قيمة افتراضية إلى الـ DATA_DIR على مستوى الـ Image بحيث أن كل Container يتم إنشاؤه من هذا الـ Image سيكون لديه هذا القيمة الافتراضية للـ DATA_DIR

FROM node:25-alpine

WORKDIR /app

COPY package*.json .

RUN npm install

COPY . .

RUN mkdir -p /app/db

ENV PORT=3000
ENV DATA_DIR=/app/db

EXPOSE 3000

ENTRYPOINT ["npm", "start"]

لاحظ أننا أضفنا RUN mkdir -p /app/db لإنشاء مجلد الذي سنخذ فيه البيانات داخل الـ Image أو بمعنى أوضح داخل أي Container يتم إنشاؤه من هذا الـ Image
ثم أضفنا ENV DATA_DIR=/app/db لتحديده كمسار تخزين البيانات
الآن كل Container يتم إنشاؤه من هذا الـ Image سيكون لديه مجلد /app/db

الآن لنعيد بناء الـ Image:

> docker image build -t nodejs-docker-app:v2.0 .
...
 => [1/6] 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.19kB                                                                                                                                                    0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                                                                          0.0s
 => CACHED [3/6] COPY package*.json .                                                                                                                                                  0.0s
 => CACHED [4/6] RUN npm install                                                                                                                                                       0.0s
 => [5/6] COPY . .                                                                                                                                                                     0.0s
 => [6/6] RUN mkdir -p /app/db                                                                                                                                                         0.6s
...

الآن لكي نستخدم الـ Volume الذي أنشأناه، نحتاج لربطه بمجلد /app/db داخل الـ Container
تذكر أن اسم الـ Volume هو products-data، لذا سنستخدم هذا الاسم في أمر docker container run مع خيار -v لربطه بمجلد /app/db داخل الـ Container

والأمر بسيط جدًا سيتم وضعه أثناء إنشاء الـ Container وهو كالتالي -v products-data:/app/db
بحيث أن products-data هو اسم الـ Volume و /app/db هو المجلد داخل الـ Container الذي نريد ربطه بهذا الـ Volume

لنرى الأمر الكامل لإنشاء الـ Container وربطه بالـ Volume:

> docker container run -d --rm --name nodejs-container -p 3000:3000 -v products-data:/app/db nodejs-docker-app:v2.0
9b9b1505bd69267b6caa30196efb51160ea8304b2db08e02334066cc51dc118f

بعد تنفيذ هذا الأمر، تم إنشاء Container جديد وربطه بالـ Volume الذي أنشأناه
الآن لنرى لنضف بعض المنتجات:

> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Laptop", "price": 1500}'
{"name":"Laptop","price":1500}

> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Phone", "price": 800}'
{"name":"Phone","price":800}

> curl http://localhost:3000/products
[{"name":"Laptop","price":1500},{"name":"Phone","price":800}]

أضفنا هنا منتجين من خلال POST /products
ثم تأكدنا أن المنتجات تم تخزينها بنجاح عن طريق GET /products
وكل هذا تم تخزينه في مجلد /app/db داخل الـ Container

لنقم بعمل ls لهذا المجلد داخل الـ Container لنرى الملفات الموجودة ونتأكد أن products.json موجود:

> docker container exec nodejs-container ls /app/db
products.json

لنتأكد من محتوى products.jsonعن طريق أمر cat:

> docker container exec nodejs-container cat /app/db/products.json
[{"name":"Laptop","price":1500},{"name":"Phone","price":800}]

حسنًا جربنا كل الطريق لتأكد أن البيانات موجودة داخل الـ Container في مجلد /app/db
الآن لنوقف الـ Container ليتم حذفه تلقائيًا بسبب خيار --rm الذي استخدمناه عند إنشائه:

> docker container stop nodejs-container
nodejs-container

الآن لنعيد إنشاء Container جديد مع نفس الـ Volume
لنتأكد هل البيانات ما زالت موجودة أم لا؟

> docker container run -d --rm --name nodejs-container -p 3000:3000 -v products-data:/app/db nodejs-docker-app:v2.0
dabfe1892e21682b386dcbe02620c3f02150a046717fc76f9ea9882ffd6ad99f

لنتحقق من وجودالبيانت عن طريق فقط جلب المنتجات من خلال GET /products:

> curl http://localhost:3000/products
[{"name":"Laptop","price":1500},{"name":"Phone","price":800}]

البيانات ما زالت موجودة لأنها محفوظة في الـ Volume وليس في الـ Container
والـ Container يقوم فقط بعمل تزامن بين المجلد /app/db والـ Volume، أي عملية sync بينهما
بالتالي أي تغيير يحدث في /app/db داخل الـ Container يتم تخزينه في الـ Volume والعكس صحيح
فلو حذفنا الـ Container مئة مرة وأعدنا إنشاءه، طالما نربطه بنفس الـ Volume فالبيانات ستبقى كما هى

+-------------------------------------------------------------------+
|                            Your Device                            |
|                                                                   |
| localhost:3000 ---+                                               |
|                   |                                               |
|               Port Mapping                                        |
|                   |                                               |
|  +----------------|--------------------------------------------+  |
|  |                |       Docker Engine                        |  |
|  |    Container   V                        Volume              |  |
|  |  +----------------------+         +----------------------+  |  |
|  |  | nodejs-container     | ------> | products-data        |  |  |
|  |  | running on port 3000 |  sync   | stored products.json |  |  |
|  |  |                      |         +----------------------+  |  |
|  |  | data that stored in  |                                   |  |
|  |  | /db/products.json    |                                   |  |
|  |  | are synced with the  |                                   |  |
|  |  | products-data volume |                                   |  |
|  |  +----------------------+                                   |  |
|  |                                                             |  |
|  +-------------------------------------------------------------+  |
|                                                                   |
+-------------------------------------------------------------------+

كما نرى في الرسم البياني، الـ Container حاولت توضيح كيف أن البيانات في مجلد /app/db داخل الـ Container يتم تخزينها في الـ Volume الذي يدعى products-data
وكل مرة يتم فيها تحديث البيانات في /app/db داخل الـ Container سيتم مزامنتها مع الـ Volume والعكس صحيح، أي أي تغيير في الـ Volume سيتم مزامنته مع /app/db داخل الـ Container
وأيضًا كما قلنا في حال تم حذف الـ Container سيظل الـ Volume موجودًا مع البيانات المخزنة فيه، وعندما نعيد إنشاء Container جديد وربطه بنفس الـ Volume، البيانات ستظهر داخل هذا الـ Container الجديد لأنه ارتبط بنفس الـ Volume

لنوقف الـ Container:

> docker container stop nodejs-container
nodejs-container

عندما تقوم بتفقد مجلد المشروع في جهازك الشخصي فأنت لن تجد /db أو products.json

> ls
app.js  Dockerfile  node_modules  package-lock.json  package.json

لأن هذه الملفات تنشيء داخل الـ Container ويتم تخزينها في الـ Volume الذي يديره الـ Docker وليس في مجلد المشروع على جهازك الشخصي
وهذا يعد ميزة كبيرة في الـ Volume لأنه يعزل البيانات سواء عن الـ Container أو عن أي مجلد في جهازك الشخصي

إنشاء Volume تلقائيًا

في الحقيقة لا نحتاج دائمًا لإنشاء الـ Volume مسبقًا بأمر docker volume create
إذا استخدمنا اسم Volume غير موجود في أمر -v، الـ Docker سينشئه تلقائيًا:

> docker container run -d --rm --name nodejs-container -p 3000:3000 -v my-new-volume:/app/db nodejs-docker-app:v2.0

هنا نحن كتبنا -v my-new-volume:/app/db، و my-new-volume هو اسم Volume غير موجود ولم نقم بإنشائه مسبقًا
لذا الـ Docker أنشأ Volume جديد اسمه my-new-volume وربطه بمجلد /app/db داخل الـ Container
الآن لنرى الـ Volumes الموجودة:

> docker volume list
DRIVER    VOLUME NAME
local     my-new-volume
local     products-data

لاحظ أن my-new-volume تم إنشاؤه تلقائيًا بدون ما نحتاج ننشئه يدويًا
على أي حال طالما أنه Volume جديد، فهو فارغ ولا يحتوي على أي بيانات
لذا إذا قمنا بعمل GET /products الآن، فلن نجد أي منتجات
لأن البيانات مخونة في الـ Volume القديم products-data وليس في my-new-volume الجديد

لنوقف الـ Container:

> docker container stop nodejs-container
nodejs-container

حذف Volumes

لحذف Volume معين نستخدم أمر docker volume remove متبوعًا باسم الـ Volume:

> docker volume remove my-new-volume
my-new-volume

ملحوظة: لدينا أمر docker volume prune الذي يقوم بحذف كل الـ Volumes التي بلا اسم والتي تدعى Anonymous Volumes
ولدينا أمر docker volume prune -a الذي يقوم بحذف كل الـ Volumes الغير مستخدمة حتى لو كانت لها أسماء
لكن عليك الحذر عند استخدام أي أمر من أوامر الحذف لكي لا تحذف أي Volume مهم عن طريق الخطأ

تعريف Volume على مستوى الـ Image

في كل الأمثلة السابقة كنا نحدد الـ Volume عند إنشاء الـ Container باستخدام -v
لكن في الحقيقة يمكننا تعريف الـ Volume مباشرة داخل الـ Dockerfile نفسه باستخدام أمر VOLUME
هكذا أي Container يتم إنشاؤه من هذا الـ Image سيكون لديه هذا الـ Volume معرف مسبقًا ولن تحتاج لتحديده عند كل مرة تنشئ فيها Container

لنعدل الـ Dockerfile الخاص بتطبيقنا:

FROM node:25-alpine

WORKDIR /app

COPY package*.json .

RUN npm install

COPY . .

RUN mkdir -p /app/db

ENV PORT=3000
ENV DATA_DIR=/app/db
EXPOSE 3000

VOLUME ["/app/db"]

ENTRYPOINT ["npm", "start"]

لاحظ السطر الجديد VOLUME ["/app/db"]
هذا يخبر الـ Docker أن المجلد /app/db داخل الـ Container يجب أن يكون Volume
لكن أننا لم نحدد اسم لهذا الـ Volume، لأن الأمر VOLUME لا يسمح لنا بتحديد اسم، بل فقط يحدد المجلد داخل الـ Container الذي سيكون Volume
لذا عندما نستخدم VOLUME في الـ Dockerfile، كل Container يتم إنشاؤه من هذا الـ Image سيكون لديه Volume مرتبط بمجلد /app/db، لكن هذا Volume سيكون بلا اسم، أي سيكون Anonymous Volume
وكل مرة ننشيء Container سيكون له Volume مختلف عن الآخر، وهذا يعني أن البيانات لن تنتقل بين Containers مختلفة لأن كل Container سيكون له Volume خاص به

لنرى هذا عمليًا ولنبني الـ Image بالتعديلات الجديدة:

> docker image build -t nodejs-docker-app:v3.0 .
...
 => [1/6] 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: 130B                                                                                                                                                      0.0s
 => CACHED [2/6] WORKDIR /app                                                                                                                                                          0.0s
 => CACHED [3/6] COPY package*.json .                                                                                                                                                  0.0s
 => CACHED [4/6] RUN npm install                                                                                                                                                       0.0s
 => CACHED [5/6] COPY . .                                                                                                                                                              0.0s
 => CACHED [6/6] RUN mkdir -p /app/db                                                                                                                                                  0.0s
...

الآن لنشغل الـ Container بدون ما نحدد -v

> docker container run -d --rm --name nodejs-container -p 3000:3000 nodejs-docker-app:v3.0
b19e2934c741606cb20aa1cac15b096e27b9bb550e60b12be71e807e3a64ad2e

في الواقع دعنا ننشيء Container آخر بدون -v أيضًا:

> docker container run -d --rm --name nodejs-container-2 -p 3001:3000 nodejs-docker-app:v3.0
e8b8b3657bb8a30e5d1b192edc2d0c4000ec4f6c1a40db1691846a1c4d576c57

الآن لدينا Container يدعى nodejs-container يعمل على port رقم 3000 و Container آخر يدعى nodejs-container-2 يعمل على port رقم 3001
كلاهما يستخدم نفس الـ Image الذي يحتوي على أمر VOLUME التي تحدد أن مجلد /app/db هو Volume
ولم نستخدم -v عند إنشاء أي من الـ Containers، لذا كل Container لديه Volume خاص به تم إنشاؤه تلقائيًا من قبل الـ Docker بسبب أمر VOLUME في الـ Dockerfile

لنرى الـ Volumes الموجودة لدي الآن:

> docker volume list
DRIVER    VOLUME NAME
local     2f553db774f32b1b30c545cdacbd71166e94acf454a492adcb8b67b3d598f115
local     02a9e669872ef700c2b3a030fb27006d94f636d7c3218b99a54550a0d7fc1ea3
local     products-data

كما قلنا كل Container لديه Volume خاص به تم إنشاؤه تلقائيًا من قبل الـ Docker بسبب أمر VOLUME في الـ Dockerfile
وستلاحظ أن الـ Volume الخاص بكل Container له اسم غريب يتكون من hash عشوائي، وهذا هو شكل الـ Anonymous Volume الذي يتم إنشاؤه تلقائيًا بدون اسم

لنضف بعض المنتجات ونتأكد أن كل شيء يعمل:

> curl -X POST http://localhost:3000/products -H "Content-Type: application/json" -d '{"name": "Tablet", "price": 600}'
{"name":"Tablet","price":600}

> curl http://localhost:3000/products
[{"name":"Tablet","price":600}]

أضفنا منتج جديد في nodejs-container، والآن لنجرب جلب المنتجات من nodejs-container-2:

> curl http://localhost:3001/products
[]

سنجدها فارغة لأن كل Container لديه Volume خاص به، والبيانات التي أضفناها في nodejs-container تم تخزينها في الـ Volume الخاص به، أما nodejs-container-2 فلديه Volume خاص به لا يحتوي على أي بيانات

الـ Anonymous Volumes

لنوقف الـ Containers:

> docker container stop nodejs-container nodejs-container-2
nodejs-container
nodejs-container-2

الآن تم ايقاف وحذف الـ Containers، بسبب خيار --rm
لكن خيار --rm يقوم أيضًا بحذف الـ Anonymous Volumes المرتبطة بالـ Containers تلقائيًا، لذا البيانات التي كانت مخزنة في هذه الـ Volumes ستفقد

> docker volume list
DRIVER    VOLUME NAME
local     products-data

لكن لو أننا قمنا بإنشاء Container بدون --rm ثم قمنا بإيقاف وحذف الـ Container يدويًا باستخدام docker container remove، في هذه الحالة الـ Anonymous Volume المرتبط بهذا الـ Container لن يتم حذفها تلقائيًا، بل سيبقى موجودًا حتى بعد حذف الـ Container

يمكننا تجربة الأمر عمليًا سنقوم بإنشاء Container بدون --rm و Container آخر مع --rm:

> docker container run -d --name nodejs-container -p 3000:3000 nodejs-docker-app:v3.0
fb76137f8b5271a78737e447cc1135e40d913f71780664ef003659b2aaa6d176

> docker container run -d --rm --name nodejs-container-2 -p 3001:3000 nodejs-docker-app:v3.0
93a34a6e433663d98407ea48180d4d65f31a7c2d8103c8cadd54339753086bc7

الآن لنرى الـ Volumes الموجودة:

> docker volume list
DRIVER    VOLUME NAME
local     84a1d9ca1c219b0e86f41e3b76c078aea193ac3d2b540ea0734751a2c20d0ac5
local     2526a10d45ec8c56f7b6b521161b98c93c9fd82eebe148b147178af372e12c54
local     products-data

الآن لدينا Volume خاص بكل Container، وكلاهما Anonymous Volumes بسبب أمر VOLUME في الـ Dockerfile
والـ Container الأول nodejs-container تم إنشاؤه بدون --rm
أما nodejs-container-2 تم إنشاؤه مع --rm

الآن سنقوف بإيقاف الـ Container الأول ثم نحذفه يدويًا:

> docker container stop nodejs-container
nodejs-container

> docker container remove nodejs-container
nodejs-container

لنرى الـ Volumes الموجودة:

> docker volume list
DRIVER    VOLUME NAME
local     84a1d9ca1c219b0e86f41e3b76c078aea193ac3d2b540ea0734751a2c20d0ac5
local     2526a10d45ec8c56f7b6b521161b98c93c9fd82eebe148b147178af372e12c54
local     products-data

لاحظ أن الـ Volume كما هى لم يتم حذفها، بل ما زالت موجودة رغم أن الـ Container تم حذفه

الآن لنقم بإيقاف الـ Container الثاني الذي تم إنشاؤه مع --rm:

> docker container stop nodejs-container-2
nodejs-container-2

الآن تم إيقاف الـ Container الثاني، وبما أنه تم إنشاؤه مع --rm، فهذا يعني أنه سيتم حذفه تلقائيًا عند الإيقاف
وأيضًا الـ Volume المرتبط به سيتم حذفه أيضًا تلقائيًا لأنه Anonymous Volume وبسبب --rm

> docker volume list
DRIVER    VOLUME NAME
local     2526a10d45ec8c56f7b6b521161b98c93c9fd82eebe148b147178af372e12c54
local     products-data

الـ Volume المرتبط بالـ Container الثاني تم حذفه تلقائيًا، أما الـ Volume المرتبط بالـ Container الأول ما زال موجودًا لأنه تم إنشاؤه بدون --rm

لذا عليك الحذر عند استخدام --rm مع الـ Anonymous Volumes لأنه سيؤدي إلى حذف الـ Volume تلقائيًا مع حذف الـ Container، وبالتالي فقدان البيانات المخزنة في هذا الـ Volume كما رأينا للتو

لنقم بحذف الـ Anonymous Volume المتبقي:

> docker volume remove 2526a10d45ec8c56f7b6b521161b98c93c9fd82eebe148b147178af372e12c54
2526a10d45ec8c56f7b6b521161b98c93c9fd82eebe148b147178af372e12c54

يمكننا دائما عمل override لأمر VOLUME عند إنشاء الـ Container باستخدام -v كما فعلنا في الأمثلة السابقة، فإذا قمنا بتحديد -v عند إنشاء الـ Container، فهذا سيحل محل أمر VOLUME في الـ Dockerfile
وتذكر أن --rm تحذف فقط الـ Anonymous Volume وليس لها تأثير على الـ Named Volumes، فإذا قمنا بإنشاء Container مع -v لربطه بـ Named Volume، فهذا الـ Volume لن يتم حذفه تلقائيًا حتى لو استخدمنا --rm

ويمكننا إنشاء Anonymous Volume بدون أمر VOLUME في الـ Dockerfile، فقط عن طريق إنشاء Container مع -v بدون تحديد اسم Volume، مثل -v /app/db، هذا سيؤدي إلى إنشاء Anonymous Volume مرتبط بمجلد /app/db داخل الـ Container

> docker container run -d --name nodejs-container -p 3000:3000 -v /app/db nodejs-docker-app:v3.0
32f791688006e213d3413edd14dd4b1ee41c1c888347fb438dbb2116ffcae3b2

لاحظ أننا استخدمنا -v /app/db بدون تحديد اسم Volume، هذا يعني أن الـ Docker سينشئ Anonymous Volume تلقائيًا مرتبط بمجلد /app/db داخل الـ Container
هذه الطريقة ستعمل حتى لو لم يكن هناك أمر VOLUME في الـ Dockerfile

> docker volume list
DRIVER    VOLUME NAME
local     6483f570bb8cd71c4494fb77088d4d8aed0ca606ac608d2ce6b936b71b5a3fef
local     products-data

لاحظ أننا لم نستخدم --rm عند إنشاء الـ Container، لذلك الـ Anonymous Volume سيظل موجودًا حتى بعد حذف الـ Container
لكن يمكننا حذف الـ Container مع كل الـ Anonymous Volumes المرتبطة به باستخدام أمر docker container remove مع خيار -v

أولًا سنوقف الـ Container:

> docker container stop nodejs-container
nodejs-container

ثم سنحذف الـ Container مع الـ Anonymous Volume المرتبط به باستخدام -v:

> docker container remove -v nodejs-container
nodejs-container

> docker volume list
DRIVER    VOLUME NAME
local     products-data

لاحظ أن الـ Anonymous Volume المرتبط بالـ Container تم حذفه تلقائيًا بسبب خيار -v في أمر docker container remove
والآن لم يتبقى سوى الـ Volume الذي أنشأناه يدويًا في البداية products-data

ملحوظات حول الـ Anonymous Volumes

ستلاحظ أن الـ Anonymous Volume الذي ينشئه الـ Docker تلقائيًا له عدة مشاكل وهى أننا في كل مرة ننشيء Container جديد سيتم إنشاء Volume جديد بـ hash مختلف
وطالما أن الـ Volume بلا اسم، فلا يمكننا ربطه بأي Container ولا يمكننا إعادة استخدامه، وبالتالي إذا أردنا أن نحتفظ بالبيانات بين Containers مختلفة، فهذا غير ممكن مع Anonymous Volumes

ويمكننا دائمًا عمل override لأمر VOLUME عند إنشاء الـ Container باستخدام -v كما فعلنا في الأمثلة السابقة

لذا قد يخطر في بالك سؤال وهو ما فائدة أمر VOLUME في الـ Dockerfile إذا كان لا يمكننا إعادة استخدامه ؟
وسيتم حذفه تلقائيًا عند حذف الـ Container إذا تم إنشاؤه مع --rm
وفوق هذا يمكننا دائمًا عمل override له
لذا ما فائدة الـ Anonymous Volumes من الأساس ؟

يمكننا القول أن الفائدة من أمر VOLUME في الـ Dockerfile قد تكون مثل فائدة الـ EXPOSE في الـ Dockerfile
بحيث أنها توثيق يوضح لأي شخص يقرأ الـ Dockerfile أن هذا المشروع يحتوي على بيانات تحتاج لربطه بـ Volume عند إنشاء الـ Container
لأن الشخص الذي يقرأ الـ Dockerfile سيرى أمر VOLUME وسيعلم أن هذا المشروع يحتاج لربطه بـ Volume عند إنشاء الـ Container

أما بالنسبة لفائدة الـ Anonymous Volumes، فهي أنها توفر نوع من الأمان والحماية من فقدان البيانات عن طريق الخطأ
بالتالي في حالة أنك نسيت ربط الـ Container بـ Volume، فسيتم إنشاء Anonymous Volume تلقائيًا بسبب أمر VOLUME، وبالتالي لن تضيع البيانات حتى لو نسيت تحديد -v عند إنشاء الـ Container في حالة عدم استخدام --rm بالطبع

وأيضًا تضمن أن البيانات لا تخزن داخل الـ Container نفسه، بل في Volume مستقل عن الـ Container


في الواقع ستجد أمر VOLUME مستخدمة بكثرة في الـ Images الرسمية لقواعد البيانات مثل MySQL و PostgreSQL و MongoDB
فمثلًا الـ Dockerfile الرسمي لـ MySQL يحتوي على:

VOLUME /var/lib/mysql

بالتالي عندما نقوم بعمل Container من هذا الـ MySQL Image سيتم إنشاء Anonymous Volume تلقائيًا مرتبط بمجلد /var/lib/mysql داخل الـ Container في حالة عدم تحديد -v عند إنشاء الـ Container
وإذا قمت بحذف الـ Container ستظل البيانات موجودة في الـ Volume كنوع من أنواع الأمان والحماية من فقدان البيانات عن طريق الخطأ

لأنك قد تنسى تحديد -v عند إنشاء الـ Container ثم تخزن بيانات مهمة في الـ MySQL داخل الـ Container، ثم بعد فترة تم حذف الـ Container عن طريق الخطأ، في هذه الحالة البيانات ستبقى في الـ Volume ولن تضيع

ملحوظة: حتى مع وجود VOLUME في الـ Dockerfile، يُفضل دائمًا استخدام -v عند إنشاء الـ Container لتحديد اسم واضح للـ Volume والابتعاد عن الـ Anonymous Volumes قدر الإمكان
بهذه الطريقة تضمن أن الـ Container الجديد سيستخدم نفس البيانات القديمة المشتركة الخاصة بتطبيقك، بدلاً من إنشاء Anonymous Volume جديد فارغ في كل مرة
وغاليبًا ما ستقوم بإنشاء Volume دائم وخاص بكل مشروع ليكون ثابت مع كل Container يتم إنشاؤه من نفس الـ Image

الخلاصة

لقد وصل عدد سطور المقالة لألف سطر، لذا أظن أننا نستطيع الإكتفاء بهذا القدر عن الـ Volumes في هذا المقال
لقد قمنا بتغطية كل المفاهيم الأساسية المتعلقة بالـ Volumes وكيفية استخدامها لحفظ البيانات بشكل دائم خارج الـ Container
وشرحنا الفرق بين الـ Volumes العادي والذي يسمى Named Volumes وبين الـ Anonymous Volumes وكيفية إنشاء كل منهما واستخدامه

على أي حال، في هذه المقالة تعلمنا أمور كثيرة عن الـ Volumes، منها:

  • لماذا تضيع البيانات عند حذف الـ Container وشاهدنا ذلك عمليًا مع تطبيقنا
  • كيف نستخدم الـ Volumes لحفظ بيانات التطبيق بشكل دائم خارج الـ Container
  • كيف ننشئ Volume ونربطه بالـ Container وكيف يتم إنشاؤه تلقائيًا
  • كيف نعرف Volume على مستوى الـ Image باستخدام أمر VOLUME في الـ Dockerfile
  • الفرق بين الـ Named Volumes والـ Anonymous Volumes ومتى نستخدم كل منهما
  • كيف نعدل تطبيقنا ليدعم تخزين البيانات في مجلد منفصل عبر environment variable

في المقالة القادمة سنتعرف على الـ Bind Mounts وهي طريقة أخرى لحفظ البيانات لكنها مختلفة عن الـ Volumes وتُستخدم بشكل أكبر أثناء التطوير

أرجو أن تكون قد استفدت من هذه المقالة

ملخص الأوامر

الأمر الوظيفة
docker system df عرض استخدام موارد الـ Docker من Images و Containers و Volumes
docker system prune --all --volumes --force حذف جميع الموارد غير المستخدمة
docker volume create <name> إنشاء Named Volume
docker volume list عرض جميع الـ Volumes
docker volume inspect <volume> عرض تفاصيل Volume معين
docker volume remove <volume> حذف Volume
docker container run -v <volume>:/app/db <image> إنشاء Container مع ربطه بـ Named Volume
docker container run -v /app/db <image> إنشاء Container مع Anonymous Volume
docker container remove -v <container> حذف Container مع الـ Anonymous Volumes المرتبطة به