حفظ البيانات في 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المعلقة التي لا تملك اسم أوTagdocker image prune --allلحذف كل الـImagesالغير مستخدمة أو التي لا تملك اسم أوTagdocker container pruneلحذف الـContainersالمتوقفةdocker volume pruneلحذف الـVolumesالتي لا تملك اسمdocker volume prune --allلحذف كل الـVolumesالغير مستخدمة أو التي لا تملك اسمdocker builder pruneلحذف الـBuild Cachedocker builder prune --allلحذف كل الـBuild Cacheالخاصة بكل الـImagesdocker system pruneلحذف كل ما سبق لكن بدون--alldocker 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 المرتبطة به |