Docker Compose: الفرق بين bind mount وnamed volume
تعرّف متى تستخدم bind mount للإعدادات وnamed 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 mount في الغالب
هذا هو الفرق العملي الأكبر، وينتج في الغالب عن قاعدة واحدة: تتم تهيئة named volume الفارغ عند أول استخدام من image، بينما لا تتم تهيئة bind mount بهذه الطريقة مطلقاً.
عندما يركّب Docker named volume فارغاً فوق مجلد يحتوي مسبقاً على محتوى في image، ينسخ ذلك المحتوى إلى volume، مع الملكية والصلاحيات التي حدّدتها image. تأتي image الرسمية لـPostgres مع /var/lib/postgresql/data مملوكاً لمستخدمها postgres، لذلك يصبح volume مملوكاً للمعرّف الرقمي نفسه، وتبدأ قاعدة البيانات.
هذه التهيئة هي نقطة التقدم الوحيدة التي يحصل عليها named volume. يدير Docker مكان وجود volume، لكن الحاويات تقرأه وتكتب فيه باستخدام عمليات نظام الملفات العادية، لذلك تُفرض الملكية والصلاحيات بالطريقة نفسها كما في bind mount. إذا رُكّب named volume على مسار لا يوجد في image، أو مُلئ لأول مرة بواسطة image أو مستخدم مختلف، فلن يحصل على أي ملكية من image، وقد يُمنع هناك أيضاً process يعمل بصلاحيات غير root. يعمل فحص id أدناه مع كلا النوعين.
يفعل bind mount العكس. كل ما يوجد على المضيف هو ما تراه الحاوية، بما في ذلك الملكية، ويُحجب محتوى image الموجود في ذلك المسار. إذا لم يكن مجلد المضيف موجوداً، ينشئه Docker daemon، ويعمل daemon بصلاحيات root، لذلك تحصل على مجلد مملوك لـroot:root. بعد ذلك لا يستطيع process داخل الحاوية، إذا كان يعمل كمستخدم غير root، الكتابة فيه:
PermissionError: [Errno 13] Permission denied: '/data/app.db'الحل هو جعل الأرقام متطابقة. تُقارن الملكية عبر bind mount باستخدام user id الرقمي، لا الاسم، لأن للحاوية /etc/passwd خاصاً بها. لا يعني مستخدم اسمه app داخل الحاوية شيئاً على المضيف. يعني uid 1000 القيمة uid 1000 على الجانبين، إلا إذا كان daemon يعمل مع user-namespace remapping (userns-remap، المضبوط في /etc/docker/daemon.json أو في سطر أوامر dockerd). ينقل remapping كل معرّف من الحاوية إلى نطاق فرعي على المضيف. في نطاق المثال لدى Docker الذي يبدأ عند 231072، يصبح container uid 0 هو host uid 231072، ولذلك يصبح container uid 1000 هو host uid 232072.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./dataيطبع docker compose exec web id الـuid الذي يعمل به process في الحاوية فعلياً. عند عدم استخدام remapping، طابق مجلد المضيف مع هذا الرقم، أو ثبّت الحاوية على رقمك باستخدام user: "1000:1000" في الخدمة. عند تفعيل remapping، لا يستطيع user: تثبيت الحاوية على رقمك في المضيف، لأن المعرّف الذي تحدّده يُنقل هو أيضاً: فما يزال user: "1000:1000" ينتهي إلى host uid 232072 في نطاق المثال. بدلاً من ذلك، غيّر مالك مجلد المضيف إلى المعرّف المقابل على المضيف؛ يوجد نطاقك الخاص في /etc/subuid و/etc/subgid. تذكر وثائق Docker --userns=host فقط على docker container create أو run أو exec بوصفه طريقة إيقاف remapping لحاوية واحدة. ويكون تثبيت user: أنظف لتطبيق كتبته بنفسك. أما تغيير مالك مجلد المضيف فيكون أكثر أماناً مع image لم تكتبها، لأن بعض images تبدأ entrypoint بصلاحيات root، ثم تخفّض الصلاحيات، وتتوقع ملكية محددة داخلها.
تجدر معرفة مشكلتين إضافيتين. في Fedora وRHEL والأنظمة الأخرى التي يكون فيها SELinux (security-enhanced Linux) في وضع enforcing، يُمنع bind mount حتى يُعاد تصنيف مساره، لذلك أضف :z لمسار مشترك بين الحاويات، أو :Z لمسار ينبغي أن تستخدمه حاوية واحدة فقط، وتُكتب الصيغة على النحو - ./data:/data:Z. كما يتعطل bind mount لملف واحد، بدلاً من مجلد، عندما يستبدل محرّر الملف بدلاً من الكتابة فيه مباشرة، لأن mount يتبع inode الأصلي. وتستمر الحاوية في رؤية المحتوى القديم حتى تعيد تشغيلها. ركّب المجلد الأب عندما يتكرر تحرير الملف.
الأداء: أين يظهر الفرق فعلياً
على خادم 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 يضبط الأذونات انطلاقاً من image، ولأن البيانات لا تعتمد على مسار في المضيف.
الحالة المختلطة هي الوسائط. تكتب الخدمة مكتبة الصور، لكنك تديرها أيضاً، وغالباً ما تكون كبيرة بما يكفي لتحتاج إلى قرص محدد. اربطها بمسار على ذلك القرص عبر bind mount، واضبط الملكية عمداً مرة واحدة. هذا هو النمط الذي تستقر عليه معظم حزم الخدمات ذاتية الاستضافة: named volumes لقواعد البيانات وذاكرات التخزين المؤقت، وbind mounts للإعدادات وللدليل الكبير الذي تهتم به. وينطبق ذلك تماماً على مكتب دعم مثل تشغيل Chatwoot على VPS، مع وضع Postgres في named volume والمرفقات المرفوعة في مسار يمكنك تحديده لعملية نسخ احتياطي.
FAQ
ما الفرق بين bind mount وnamed volume؟
يربط bind mount مساراً على host بالحاوية، لذلك يرى الجانبان المجلد نفسه، ويمكنك تعديله باستخدام الأدوات العادية. أما named volume فهو مساحة تخزين ينشئها Docker ويديرها، ويُشار إليها بالاسم وتُعرّف في كتلة volumes: العليا. ويتمثل الفرق العملي في الملكية: استخدم bind mounts للإعدادات التي تديرها، وnamed volumes للبيانات التي يديرها التطبيق.
لماذا تظهر لي رسالة "permission denied" مع bind mount ولا تظهر مع named volume؟
يُملأ named volume الفارغ من image، لذلك يرث عادةً الملكية التي حدّدتها image، ويستطيع مستخدم الحاوية الكتابة فيه. لكنه يظل يفرض الصلاحيات، ولذلك قد يفشل بالطريقة نفسها إذا لم تكن لدى image ملفات في ذلك المسار. يعرض bind mount مجلد host كما هو تماماً، وإذا اضطر Docker إلى إنشاء المجلد، أنشأه مملوكاً لـroot. شغّل docker compose exec <service> id لمعرفة المعرّف الرقمي الذي تستخدمه الحاوية، ثم sudo chown -R <uid>:<gid> على مجلد host، أو، عندما لا يعيد daemon تعيين user namespaces، اضبط user: "1000:1000" على service. وإذا كان daemon يستخدم user-namespace remapping، فغيّر الملكية إلى المعرّف المعيّن على host بدلاً من ذلك، لأن معرّفات الحاوية تُزاح على host، ولا يغيّر user: هذا الأمر.
أين يخزّن Docker named volumes على القرص؟
مع local الافتراضي، توجد هذه volumes تحت /var/lib/docker/volumes/<volume>/_data، ويطبع docker volume inspect <volume> قيمة Mountpoint الدقيقة. اقرأها إذا احتجت إلى التحقق من شيء، لكن اكتب فيها من خلال حاوية فقط، لأن تعديلها بصفة root على host يغيّر الملكية بطرق لا تتوقعها الحاوية.
كيف أنشئ نسخة احتياطية من named volume؟
شغّل حاوية قصيرة العمر مع تركيب volume ومجلد host فيها معاً، ثم أرشف من أحدهما إلى الآخر باستخدام 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، لأن ذلك المجلد تابع لـhost وليس لـDocker.