Docker Compose: الفرق بين bind وnamed volume
تعرّف إلى الفرق بين bind mount وnamed volume في Docker Compose، ومتى تستخدم كلّاً منهما للإعدادات والبيانات، مع حل خطأ تعريف volume وفحصه ونسخه ونقله.
Bind mount أو named volume: الإجابة المختصرة
تأتي وحدات التخزين في Docker Compose بنوعين، ويتعلق الاختيار بالجهة التي تملك الملفات. استخدم bind mount للملفات التي تكتبها وتقرأها بنفسك، مثل ملفات الإعداد والقوالب والمواقع الثابتة. استخدم named volume للبيانات التي يملكها التطبيق، مثل ملفات قاعدة البيانات وفهارس البحث والوسائط المرفوعة. يشير bind mount إلى مسار على الخادم المضيف يمكنك فتحه في محرر. أما named volume فهو مساحة تخزين ينشئها Docker ويتتبعها نيابةً عنك، وتصل إليها من خلال Docker.
يظهر كلا النوعين ضمن المفتاح volumes: نفسه داخل الخدمة، ولذلك يحدث الخلط بينهما. يكمن الفرق في الجانب الأيسر من النقطتين. إذا بدأ الجانب الأيسر بـ . أو /، فهو مسار على الخادم المضيف، وبالتالي يكون bind mount. وما عدا ذلك يكون اسماً، وبالتالي يكون named volume، ويجب أيضاً التصريح عن هذا الاسم في الكتلة volumes: ذات المستوى الأعلى.
صيغتا كتابة المسارات في ملف Compose
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: changeme
volumes:
- pgdata:/var/lib/postgresql/data
web:
image: nginx:1.27
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./site:/usr/share/nginx/html:ro
volumes:
pgdata:pgdata:/var/lib/postgresql/data هو volume مُسمّى. أما ./nginx.conf:/etc/nginx/nginx.conf فهو bind mount، وتربطه :ro بوضع القراءة فقط، وهو الإعداد الافتراضي الصحيح لملف إعدادات يجب ألا تعيد الحاوية الكتابة إليه مطلقاً. إذا نسيت إدخال volumes: في المستوى الأعلى، يتوقف Compose مع service "db" refers to undefined volume pgdata.
شغّل البيئة واعرض ما أنشأه Docker:
docker compose up -d
docker volume lsلا يُسمّى الـvolume pgdata. بل يُسمّى <project>_pgdata، إذ يكون اسم المشروع افتراضياً هو اسم الدليل الذي يحتوي على ملف Compose. فإذا كان اسم الدليل myapp، يصبح الاسم myapp_pgdata. هذا مهم لأن إعادة تسمية الدليل تنشئ volume جديداً فارغاً، فيبدو أن التطبيق فقد بياناته. لكنه لم يفقدها؛ فما زال الـvolume القديم ظاهراً في قائمة docker volume ls. ثبّت الاسم باستخدام name: في ملف Compose، أو اضبط COMPOSE_PROJECT_NAME إذا كان من المحتمل نقل الدليل. ضع إعدادات كهذه مع بقية ملفات بيئة Compose والأسرار لديك.
لماذا تظهر أخطاء الصلاحيات مع bind mounts فقط
هذا هو الفرق العملي الأكبر، وينتج عن قاعدة واحدة: تتم تهيئة named volume الفارغة عند أول استخدام من image، بينما لا تتم تهيئة bind mount بهذه الطريقة.
عندما يركّب Docker named volume فارغة فوق دليل يحتوي على ملفات في image، فإنه ينسخ هذه الملفات إلى volume، مع الملكية وأنماط الصلاحيات التي حدّدتها image. توفّر image الرسمية لـPostgres /var/lib/postgresql/data المملوكاً للمستخدم postgres الخاص بها، لذلك تصبح volume مملوكة للمعرّف الرقمي نفسه، وتبدأ قاعدة البيانات.
تعمل bind mount بالعكس. ما يوجد على المضيف هو ما يراه container، بما في ذلك الملكية، ويصبح محتوى image الموجود في ذلك المسار مخفياً. إذا لم يكن دليل المضيف موجوداً، ينشئه Docker daemon، ويعمل daemon بصفة root، لذلك تحصل على دليل مملوك لـroot:root. عندئذ لا يستطيع process يعمل داخل container بصفة مستخدم غير root الكتابة إليه:
PermissionError: [Errno 13] Permission denied: '/data/app.db'الحل هو جعل الأرقام متطابقة. تُقارن الملكية عبر bind mount باستخدام user id الرقمي، لا الاسم، لأن لدى container ملف /etc/passwd خاصاً به. لا يعني وجود مستخدم باسم app داخل container شيئاً على المضيف. يشير uid 1000 إلى uid 1000 على الجانبين.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./dataيطبع docker compose exec web id قيمة uid التي يعمل بها process داخل container فعلياً. اجعل دليل المضيف مملوكاً لذلك الرقم، أو ثبّت container على رقمك باستخدام user: "1000:1000" في الخدمة. يكون تثبيت user: أنظف في تطبيق كتبته بنفسك. أما تغيير مالك دليل المضيف باستخدام chown فهو أكثر أماناً مع image لم تكتبها، لأن بعض الصور تبدأ entrypoint بصفة root، ثم تخفّض الامتيازات وتتوقع ملكية محددة للملفات الداخلية.
توجد مشكلتان إضافيتان تستحقان المعرفة. في Fedora وRHEL والأنظمة الأخرى التي يكون فيها SELinux (Linux المحسّن أمنياً) في وضع enforcing، يُرفض bind mount إلى أن تُعاد تسمية سياقه، لذلك أضف :z لمسار مشترك بين عدة containers، أو :Z لمسار مخصص لاستخدام container واحد فقط، وتُكتب كالتالي: - ./data:/data:Z. كما أن bind mount لملف واحد، بدلاً من دليل، يتعطل عندما يستبدل محرر الملف بدلاً من الكتابة داخله، لأن mount يتبع inode الأصلي. يواصل container رؤية المحتوى القديم إلى أن تعيد تشغيله. ركّب الدليل الأب عندما يُحرَّر الملف كثيراً.
الأداء: أين يظهر الفرق فعلياً
على خادم Linux، يمرّ كلا النوعين عبر مسار النواة نفسه، لذلك يكون فرق معدل نقل البيانات صغيراً بما يكفي لعدم اختيار أحدهما على أساس الأداء وحده. توجد named volumes التي تستخدم برنامج التشغيل الافتراضي local على نظام الملفات نفسه الذي يستخدمه Docker، ضمن /var/lib/docker/volumes/، بينما يوجد bind mount في المكان الذي حددته له.
يظهر الفرق على Docker Desktop for macOS وWindows، حيث تعمل الحاويات داخل آلة افتراضية. يمر bind mount هناك من نظام ملفات المضيف إلى تلك الآلة الافتراضية عبر طبقة لمشاركة الملفات. لذلك تتباطأ أعباء العمل التي تنفذ عمليات كثيرة على ملفات صغيرة بشكل ملحوظ، مثل شجرة تبعيات Node.js أو ذاكرة التخزين المؤقت لإطار عمل PHP. تبقى named volumes داخل الآلة الافتراضية ولا تتحمل هذه التكلفة. ولهذا تربط ملفات Compose الخاصة بالتطوير دليل المصدر غالباً باستخدام bind mount، لكنها تعرّف named volume على node_modules.
الفرق الحقيقي الآخر هو مكان تخزين البيانات. يؤدي bind mount إلى /mnt/backup إلى وضع البيانات على ذلك القرص. أما named volume، فتُخزَّن على نظام الملفات الذي يحتوي على /var/lib/docker، وهو عادةً القرص الجذري على VPS. وعندما تكبر قاعدة بيانات داخل named volume، فإنها تملأ القرص نفسه الذي توجد عليه سجلات النظام. افحص ذلك قبل أن يتحول إلى حادثة:
docker system df -v
df -h /var/lib/dockerيعرض docker system df -v كل volume مع حجمه، ويحدد volumes التي لم تعد أي حاوية تشير إليها.
فحص وحدة تخزين مُسمّاة
الوحدة المُسمّاة ليست صندوقاً أسود. اطلب من Docker معرفة موقعها:
docker volume inspect myapp_pgdataيعرض الحقل Mountpoint مساراً حقيقياً على المضيف، ويكون عادةً /var/lib/docker/volumes/myapp_pgdata/_data. يمكنك قراءته باستخدام sudo ls، وهذا مفيد لإجراء فحص سريع. لا تتعامل معه على أنه مكان لتحرير الملفات. تؤدي الكتابة فيه بصلاحيات root إلى إعادة إنشاء مشكلة الملكية الموضحة أعلاه، كما أن هذا المسار تفصيل خاص ببرنامج التشغيل local ولا تشترك فيه برامج تشغيل وحدات التخزين الأخرى.
الطريقة الآمنة لعرض المحتوى الداخلي هي استخدام حاوية مؤقتة تربط وحدة التخزين:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volتعمل هذه الطريقة مع أي برنامج تشغيل، وتعرض الأذونات نفسها التي تراها الحاوية الفعلية، ولا تترك شيئاً خلفها بسبب --rm.
النسخ الاحتياطي لكل نوع
الـbind mount عبارة عن دليل عادي، لذلك تتعامل معه أي أداة نسخ احتياطي على مستوى الملفات. وجّه النسخ الاحتياطي إلى المسار الموجود على المضيف، وينتهي الأمر. يحتاج الـnamed volume إلى خطوة إضافية، لأن الأداة يجب أن تدخل إليه. اربط الـvolume ودليلاً من المضيف داخل الحاوية المؤقتة نفسها، ثم أنشئ أرشيفاً:
docker run --rm \
-v myapp_pgdata:/data:ro \
-v "$PWD":/backup \
alpine tar czf /backup/pgdata.tar.gz -C /data .استعد البيانات بعكس العملية إلى volume جديد:
docker volume create myapp_pgdata_restored
docker run --rm \
-v myapp_pgdata_restored:/data \
-v "$PWD":/backup \
alpine tar xzf /backup/pgdata.tar.gz -C /dataيحافظ tar على الملكية الرقمية عند تشغيله بصفة root داخل الحاوية، وهذا ما يجعل الـvolume المستعاد قابلاً للاستخدام من التطبيق.
ينطبق تحذير واحد على النوعين. يؤدي نسخ ملفات قاعدة البيانات أثناء تشغيلها إلى إنشاء أرشيف لحالة تتغير باستمرار، وقد يؤدي ذلك إلى استعادتها في حالة تالفة. أوقف الخدمة أولاً، أو أنشئ تفريغاً باستخدام أداة قاعدة البيانات نفسها، كما في docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. ينتج عن ذلك ملف عادي يمكنك تضمينه بعد ذلك في روتين نسخ احتياطي مشفّر باستخدام restic المعتاد، إلى جانب ملفات compose.
نقل bind mount إلى named volume
النقل عبارة عن نسخ وليس إعادة تسمية، ويستغرق نحو دقيقة.
docker compose down
docker volume create myapp_pgdata
docker run --rm \
-v "$PWD/data":/from \
-v myapp_pgdata:/to \
alpine sh -c 'cp -a /from/. /to/'يحافظ cp -a على الملكية والأذونات والطوابع الزمنية، لذلك يستطيع مستخدم الحاوية الذي كان قادراً على قراءة الدليل القديم قراءة الـvolume الجديد أيضاً. بعد ذلك غيّر الخدمة لاستخدام pgdata:/var/lib/postgresql/data، وأضف pgdata إلى كتلة volumes: الرئيسية، وشغّل docker compose up -d، ثم اقرأ سجلات التطبيق قبل حذف الدليل القديم. للعودة في الاتجاه الآخر، استخدم الأمر نفسه مع تبديل /from و/to.
تذكّر أمراً واحداً أثناء الاختبار. يترك docker compose down الـvolumes المسماة من دون تغيير، بينما يحذف docker compose down -v كل volume مسمى يعرّفه المشروع، ولا توجد طريقة للتراجع عن ذلك. يبقى bind mount موجوداً في الحالتين، لأن Docker لم يكن يملك ذلك الدليل قط. إذا كانت أوامر دورة الحياة لا تزال جديدة عليك، فراجع دليل أساسيات Docker Compose لخادم VPS الذي يشرحها خطوة بخطوة.
اختيار النوع لكل خدمة
اسأل: من يكتب الملف؟ الإعدادات التي تعدّلها في محرر نصوص وتثبّتها في git تنتمي إلى bind mount مركّب على :ro، لأنك تريد أن تكون مرئية ومضمّنة في إدارة الإصدارات. أما حالة التطبيق التي لا تفتحها يدوياً أبداً فتنتمي إلى named volume، لأن Docker يضبط الأذونات بطريقة صحيحة، ولأن البيانات لا تعتمد على مسار في المضيف.
الحالة المختلطة هي الوسائط. تكتب المكتبة الفوتوغرافية فيها التطبيق، لكنك تديرها أنت أيضاً، وغالباً ما تكون كبيرة بما يكفي لتحتاج إلى قرص محدد. اربطها بمسار على ذلك القرص باستخدام bind mount، واضبط الملكية عمداً مرة واحدة. هذا هو النمط الذي تستقر عليه معظم حزم الخدمات ذاتية الاستضافة: named volumes لقواعد البيانات وذاكرات التخزين المؤقت، وbind mounts للإعدادات وللدليل الكبير الذي تهتم به. وينطبق ذلك تماماً على مكتب دعم مثل Chatwoot يعمل على VPS، حيث يوجد Postgres في named volume، بينما تُخزَّن المرفقات المرفوعة في مسار يمكنك توجيه النسخ الاحتياطي إليه.
FAQ
ما الفرق بين bind mount وnamed volume؟
يربط bind mount مساراً على المضيف داخل الحاوية، لذلك يرى الطرفان الدليل نفسه ويمكنك تعديله باستخدام الأدوات المعتادة. أما named volume فهو مساحة تخزين ينشئها Docker ويديرها، ويُشار إليها بالاسم وتُعرَّف في كتلة volumes: العليا. الفرق العملي هو الملكية: استخدم bind mounts للإعدادات التي تديرها أنت، واستخدم named volumes للبيانات التي يديرها التطبيق.
لماذا تظهر الرسالة "permission denied" مع bind mount ولا تظهر مع named volume؟
تُنسخ ملكية الصورة إلى named volume الفارغ عند تهيئته، لذلك يرث الملكية التي حدّدتها الصورة ويمكن لمستخدم الحاوية الكتابة فيه. أما bind mount فيعرض دليل المضيف كما هو تماماً، وإذا اضطر Docker إلى إنشاء هذا الدليل فإنه ينشئه مملوكاً للحساب root. شغّل docker compose exec <service> id لمعرفة المعرّف الرقمي الذي تستخدمه الحاوية، ثم sudo chown -R <uid>:<gid> على دليل المضيف، أو اضبط user: "1000:1000" على الخدمة.
أين يخزّن Docker named volumes على القرص؟
باستخدام برنامج التشغيل الافتراضي local، تُخزّن هذه المساحات ضمن /var/lib/docker/volumes/<volume>/_data، ويطبع docker volume inspect <volume> قيمة Mountpoint الدقيقة. اقرأها إذا احتجت إلى التحقق من شيء، لكن اكتب إليها فقط من خلال حاوية، لأن تعديلها بصلاحيات root على المضيف يغيّر الملكية بطرق لا تتوقعها الحاوية.
كيف أنشئ نسخة احتياطية من named volume؟
شغّل حاوية قصيرة العمر مع تركيب volume ودليل من المضيف معاً، ثم أرشف البيانات من أحدهما إلى الآخر باستخدام docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data .. بالنسبة إلى قاعدة البيانات، أنشئ تفريغاً باستخدام الأداة الخاصة بقاعدة البيانات بدلاً من نسخ الملفات الحية، لأن نسخ الملفات أثناء تنفيذ عمليات الكتابة قد يُستعاد إلى حالة تالفة.
هل يحذف docker compose down وحدات التخزين الخاصة بي؟
يزيل docker compose down الحاويات والشبكات، ويُبقي named volumes في مكانها. كما يحذف docker compose down -v كل named volume يعرّفه المشروع، وبشكل نهائي. لا يحذف أيٌّ من الأمرين bind mounts، لأن ذلك الدليل يخص المضيف وليس Docker.