تفاوت Bind Mount و Named Volume در Docker Compose
تفاوت اصلی در مدیریت فایلها و دسترسی است. از Bind Mount برای فایلهای کانفیگ و از Named Volume برای دیتابیس استفاده کنید. این راهنما مشکلات دسترسی، بکآپ و مهاجرت را بررسی میکند.
Bind mount یا named volume: پاسخ کوتاه
در Docker Compose دو نوع volume وجود دارد و انتخاب بین آنها به این بستگی دارد که مالک فایلها کیست. برای فایلهایی که خودتان میخوانید و ویرایش میکنید، مانند فایلهای پیکربندی (config)، قالبها (templates) و سایتهای ایستا، از bind mount استفاده کنید. برای دادههایی که متعلق به خودِ برنامه هستند، مانند فایلهای دیتابیس، ایندکسهای جستجو و رسانههای آپلود شده، از named volume استفاده کنید. یک bind mount به مسیری روی میزبان (host) اشاره میکند که میتوانید آن را در یک ویرایشگر باز کنید. یک named volume فضایی است که Docker برای شما ایجاد و مدیریت میکند و دسترسی به آن از طریق Docker انجام میشود.
هر دو نوع در سرویسها تحت کلید یکسان volumes: قرار میگیرند و به همین دلیل اغلب با هم اشتباه گرفته میشوند. تفاوت در سمت چپ علامت دونقطه (colon) است. اگر سمت چپ با . یا / شروع شود، یک مسیر روی میزبان است و در نتیجه یک bind mount محسوب میشود. هر چیز دیگری یک نام است و در نتیجه یک named volume است؛ این نام باید در بلوک سطح بالای volumes: نیز تعریف شده باشد.
دو نحو (syntax) در فایل 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 آن را فقطخواندنی (read-only) mount میکند، که تنظیم پیشفرض مناسبی برای پیکربندیهایی است که کانتینر نباید آنها را بازنویسی کند. اگر ورودی سطح بالای 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 را تنظیم نمایید. تنظیماتی از این دست باید در کنار سایر فایلهای محیطی و secrets در Compose قرار گیرند.
چرا خطاهای مجوز عمدتاً در bind mountها رخ میدهند
این بزرگترین تفاوت عملی است و عمدتاً از یک قانون ناشی میشود: یک named volume که در اولین استفاده خالی باشد، از روی image مقداردهی اولیه (seed) میشود، اما یک bind mount هرگز اینگونه نیست.
وقتی Docker یک named volume خالی را روی دایرکتوریای که از قبل در image محتوا دارد mount میکند، آن محتوا را با مالکیت و مجوزهایی که image تعیین کرده، به volume کپی میکند. image رسمی Postgres محتوای /var/lib/postgresql/data را با مالکیت کاربر postgres خود ارائه میدهد، بنابراین volume نیز با همان شناسه عددی مالکیت مییابد و دیتابیس اجرا میشود.
آن مقداردهی اولیه، تنها مزیتی است که یک named volume دریافت میکند. Docker محل قرارگیری volume را مدیریت میکند، اما containerها آن را با عملیات معمولی فایلسیستم میخوانند و مینویسند، بنابراین مالکیت و مجوزها دقیقاً همانطور که در یک bind mount اعمال میشوند، اجرا میگردند. یک named volume که در مسیری mount شود که image آن را ندارد، یا volumeای که ابتدا توسط image یا کاربر دیگری پر شده باشد، هیچ مالکیتی از image دریافت نمیکند و ممکن است یک پردازش غیر-root نیز در آنجا با عدم دسترسی مواجه شود. بررسی id در ادامه برای هر دو نوع کاربرد دارد.
یک bind mount دقیقاً برعکس عمل میکند. هر چه روی host باشد، همان چیزی است که container میبیند (شامل مالکیت)، و محتوای image در آن مسیر مخفی میشود. اگر دایرکتوری host وجود نداشته باشد، Docker daemon آن را ایجاد میکند و چون daemon با دسترسی root اجرا میشود، دایرکتوریای با مالکیت root:root خواهید داشت. در نتیجه، پردازش container که با کاربری غیر-root اجرا میشود، نمیتواند در آن بنویسد:
PermissionError: [Errno 13] Permission denied: '/data/app.db'راهحل این است که اعداد را با هم هماهنگ کنید. مالکیت در یک bind mount بر اساس شناسه عددی کاربر (uid) مقایسه میشود، نه نام، زیرا container فایل /etc/passwd مخصوص خود را دارد. کاربری به نام app در داخل container، روی host هیچ معنایی ندارد. Uid 1000 در هر دو طرف به معنای uid 1000 است، مگر اینکه daemon با قابلیت user-namespace remapping اجرا شود (userns-remap، که در /etc/docker/daemon.json یا در خط فرمان dockerd تنظیم میشود). Remapping هر شناسه container را به یک محدوده فرعی روی host منتقل میکند. در محدوده نمونه Docker که از 231072 شروع میشود، uid 0 در container برابر با uid 231072 در host است، بنابراین uid 1000 در container برابر با uid 232072 در host خواهد بود.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./dataدستور docker compose exec web id شناسه عددیای که پردازش container واقعاً با آن اجرا میشود را چاپ میکند. بدون remapping، دایرکتوری host را با آن عدد مطابقت دهید، یا container را با استفاده از user: "1000:1000" در سرویس، به عدد خودتان محدود (pin) کنید. با فعال بودن remapping، دستور user: نمیتواند container را به عدد host شما محدود کند، زیرا شناسهای که تنظیم میکنید نیز جابهجا میشود: user: "1000:1000" در آن محدوده نمونه، همچنان به uid 232072 در host ختم میشود. در عوض، مالکیت دایرکتوری host را با دستور chown به شناسه host نگاشتشده تغییر دهید (محدوده خودتان در /etc/subuid و /etc/subgid قرار دارد). مستندات Docker تنها از --userns=host در docker container create، run یا exec به عنوان روشی برای غیرفعال کردن remapping برای یک container خاص نام میبرد. محدود کردن user: برای برنامهای که خودتان نوشتهاید تمیزتر است. تغییر مالکیت دایرکتوری host برای imageای که خودتان ننوشتهاید ایمنتر است، زیرا برخی imageها یک entrypoint را به عنوان root اجرا میکنند، دسترسیها را کاهش میدهند و انتظار مالکیت خاصی در زیرمجموعه خود دارند.
دانستن دو نکته دیگر نیز مفید است. در Fedora، RHEL و سایر سیستمهایی که SELinux (امنیت پیشرفته لینوکس) را اعمال میکنند، یک bind mount تا زمانی که برچسبگذاری مجدد (relabel) نشود، رد میشود؛ بنابراین برای مسیری که بین containerها به اشتراک گذاشته شده :z و برای مسیری که فقط یک container باید از آن استفاده کند :Z را اضافه کنید که به صورت - ./data:/data:Z نوشته میشود. همچنین، bind mount کردن یک فایل تکی (به جای دایرکتوری) زمانی که یک ویرایشگر فایل را جایگزین میکند (به جای نوشتن در همان محل)، دچار مشکل میشود، زیرا mount از inode اصلی پیروی میکند. container تا زمانی که آن را restart نکنید، محتوای قدیمی را میبیند. اگر فایل زیاد ویرایش میشود، دایرکتوری والد آن را mount کنید.
عملکرد: جایی که تفاوت واقعی است
در یک سرور Linux، هر دو نوع mount از مسیر هسته (kernel path) یکسانی عبور میکنند، بنابراین تفاوت در نرخ انتقال داده (throughput) به قدری ناچیز است که نباید مبنای انتخاب شما باشد. Named volumeها که از درایور پیشفرض local استفاده میکنند، روی همان فایلسیستمی قرار دارند که سایر بخشهای Docker در مسیر /var/lib/docker/volumes/ قرار گرفتهاند، در حالی که bind mount در هر مسیری که شما تعیین کنید قرار میگیرد.
این شکاف عملکردی در Docker Desktop برای macOS و Windows نمایان میشود، جایی که کانتینرها درون یک ماشین مجازی اجرا میشوند. در آنجا، یک bind mount از فایلسیستم میزبان (host) و از طریق یک لایه اشتراکگذاری فایل وارد ماشین مجازی میشود؛ به همین دلیل، بارهای کاری که شامل عملیاتهای پرتعداد روی فایلهای کوچک هستند—مانند درخت وابستگیهای Node.js یا کش فریمورکهای PHP—بهطور محسوسی کند میشوند. Named volumeها درون ماشین مجازی باقی میمانند و این هزینه را متحمل نمیشوند. به همین دلیل است که بسیاری از فایلهای compose برای محیط توسعه، دایرکتوری سورس را bind-mount میکنند اما برای مسیر node_modules یک named volume تعریف میکنند.
تفاوت واقعی دیگر در محل ذخیره بایتهاست. یک bind mount به مسیر /mnt/backup، دادهها را روی همان دیسک قرار میدهد. یک named volume روی هر فایلسیستمی که مسیر /var/lib/docker را در بر دارد قرار میگیرد که در یک VPS معمولاً همان دیسک ریشه (root disk) است. رشد یک دیتابیس درون یک named volume باعث پر شدن همان دیسکی میشود که لاگهای سیستم شما روی آن قرار دارند. پیش از آنکه این موضوع به یک حادثه تبدیل شود، آن را بررسی کنید:
docker system df -v
df -h /var/lib/dockerدستور docker system df -v تمام volumeها را به همراه حجم آنها فهرست میکند و مواردی که دیگر توسط هیچ کانتینری استفاده نمیشوند را مشخص مینماید.
بررسی یک named volume
یک named volume جعبه سیاه نیست. از Docker بپرسید که کجا قرار دارد:
docker volume inspect myapp_pgdataفیلد Mountpoint یک مسیر واقعی روی host ارائه میدهد که معمولاً /var/lib/docker/volumes/myapp_pgdata/_data است. میتوانید آن را با sudo ls بخوانید که برای یک بررسی سریع مفید است. با این حال، به آن به چشم مکانی برای ویرایش فایلها نگاه نکنید. نوشتن در آن مسیر با دسترسی root، مشکل مالکیت فایل که پیشتر توضیح داده شد را دوباره ایجاد میکند و این مسیر، جزئیاتی از درایور local است که سایر درایورهای volume آن را ندارند.
روش ایمن برای مشاهده محتویات، استفاده از یک container موقت است که volume را mount میکند:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volاین روش برای هر درایوری کار میکند، همان مجوزهایی را میبیند که container اصلی میبیند و به دلیل استفاده از --rm، هیچ اثری از خود باقی نمیگذارد.
پشتیبانگیری از هر نوع
یک bind mount در واقع یک دایرکتوری معمولی است، بنابراین هر ابزار پشتیبانگیری در سطح فایل بهراحتی آن را مدیریت میکند. کافی است ابزار پشتیبانگیری را به مسیر host اشاره دهید تا کار تمام شود. یک named volume به یک گام اضافی نیاز دارد، زیرا ابزار باید به درون آن دسترسی پیدا کند. volume و یک دایرکتوری host را به یک container موقت متصل (mount) کنید و سپس یک آرشیو بنویسید:
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 در داخل container، مالکیت عددی فایلها را حفظ میکند؛ این همان چیزی است که باعث میشود volume بازیابیشده توسط برنامه قابل استفاده باقی بماند.
یک هشدار برای هر دو نوع صدق میکند. کپی کردن فایلهای یک دیتابیس در حالی که دیتابیس در حال اجراست، آرشیوی از یک هدف متحرک ایجاد میکند و ممکن است هنگام بازیابی به وضعیت خراب (corrupt) منجر شود. ابتدا سرویس را متوقف کنید یا از طریق ابزار اختصاصی خود دیتابیس، خروجی بگیرید؛ همانطور که در docker compose exec -T db pg_dump -U postgres appdb > appdb.sql آمده است. این کار یک فایل ساده تولید میکند که میتوانید آن را در کنار فایلهای compose خود، در یک روال پشتیبانگیری رمزنگاریشده restic قرار دهید.
مهاجرت از bind mount به named volume
این جابهجایی یک عملیات کپی است، نه تغییر نام، و حدود 1 دقیقه زمان میبرد.
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 مالکیت، مجوزها و زمانبندیها (timestamps) را حفظ میکند، بنابراین کاربری در container که میتوانست دایرکتوری قدیمی را بخواند، همچنان میتواند volume جدید را بخواند. سپس سرویس را تغییر دهید تا از pgdata:/var/lib/postgresql/data استفاده کند، pgdata را به بلوک volumes: در سطح بالا اضافه کنید، docker compose up -d را اجرا کنید و پیش از حذف دایرکتوری قدیمی، لاگهای برنامه را بخوانید. برای انجام این کار در جهت معکوس، از همان دستور با جابهجایی /from و /to استفاده کنید.
هنگام تست کردن، یک نکته را در نظر داشته باشید. docker compose down به named volumeها کاری ندارد، اما docker compose down -v تمام named volumeهایی که در پروژه تعریف شدهاند را حذف میکند و راه بازگشتی برای آن وجود ندارد. یک bind mount از هر دو دستور جان سالم به در میبرد، زیرا Docker هرگز مالک آن دایرکتوری نبوده است. اگر دستورات چرخهٔ حیات (lifecycle) برای شما تازگی دارند، راهنمای مقدماتی Docker Compose برای VPS آنها را بهطور کامل بررسی کرده است.
انتخاب، سرویس به سرویس
بپرسید چه کسی فایل را مینویسد. پیکربندیای که در یک ویرایشگر متن ویرایش میکنید و به git کامیت میکنید، متعلق به یک bind mount است که در :ro مانت شده است، زیرا میخواهید آن را ببینید و نسخهگذاری کنید. وضعیت (state) برنامه که هرگز با دست باز نمیکنید، متعلق به یک named volume است، زیرا Docker مجوزها را از image میگیرد و دادهها به مسیر میزبان (host path) وابسته نیستند.
حالت ترکیبی، رسانهها هستند. یک کتابخانه عکس توسط برنامه نوشته میشود اما توسط شما نیز مدیریت میشود و اغلب آنقدر بزرگ است که بخواهید روی یک دیسک خاص باشد. آن را به مسیری روی آن دیسک bind mount کنید و مالکیت آن را یکبار بهطور آگاهانه تنظیم کنید. این الگویی است که اکثر stackهای self-hosted بر آن توافق دارند: named volume برای پایگاهدادهها و کشها، و bind mount برای پیکربندی و دایرکتوری بزرگی که برایتان اهمیت دارد. یک میز پشتیبانی مانند Chatwoot در حال اجرا روی یک VPS دقیقاً به همین نقطه میرسد؛ با Postgres در یک named volume و فایلهای پیوست آپلودشده در مسیری که میتوانید یک بکآپ را به آن هدایت کنید.
FAQ
تفاوت بین bind mount و named volume چیست؟
یک bind mount مسیری روی host را به داخل container نگاشت میکند، بنابراین هر دو طرف دایرکتوری یکسانی را میبینند و میتوانید آن را با ابزارهای معمولی ویرایش کنید. یک named volume فضایی ذخیرهسازی است که Docker آن را ایجاد و مدیریت میکند، با نام به آن ارجاع داده میشود و در بلوک سطح بالای volumes: تعریف میگردد. تفاوت عملی در مالکیت است: bind mountها برای تنظیماتی که شما نگهداری میکنید، و named volumeها برای دادههایی که برنامه نگهداری میکند.
چرا با bind mount خطای "permission denied" دریافت میکنم اما با named volume نه؟
یک named volume خالی از روی image مقداردهی اولیه میشود، بنابراین معمولاً مالکیتی که image تعیین کرده را به ارث میبرد و کاربر container میتواند در آن بنویسد. این volume همچنان مجوزها را اعمال میکند، بنابراین اگر image در آن مسیر محتوایی نداشته باشد، ممکن است به همان شکل با خطا مواجه شود. یک bind mount دایرکتوری host را دقیقاً همانطور که هست نشان میدهد و اگر Docker مجبور به ایجاد آن دایرکتوری شده باشد، مالکیت آن را به root اختصاص داده است. دستور docker compose exec <service> id را اجرا کنید تا شناسه عددی مورد استفاده container را ببینید، سپس sudo chown -R <uid>:<gid> را روی دایرکتوری host اعمال کنید، یا زمانی که daemon از user-namespace remapping استفاده نمیکند، user: "1000:1000" را در سرویس تنظیم کنید. اگر daemon از user-namespace remapping استفاده میکند، مالکیت را به شناسه host نگاشتشده تغییر دهید، زیرا شناسههای container روی host جابهجا شدهاند و user: آن را تغییر نمیدهد.
Docker کجا named volumeها را روی دیسک ذخیره میکند؟
با درایور پیشفرض local، آنها در مسیر /var/lib/docker/volumes/<volume>/_data قرار دارند و docker volume inspect <volume> مسیر دقیق Mountpoint را چاپ میکند. اگر نیاز به بررسی چیزی دارید آن را بخوانید، اما فقط از طریق یک container در آن بنویسید، زیرا ویرایش به عنوان root روی host، مالکیت را به شکلی تغییر میدهد که container انتظار آن را ندارد.
چگونه از یک named volume نسخه پشتیبان تهیه کنم؟
یک container موقت با volume و یک دایرکتوری host که هر دو mount شدهاند اجرا کنید، سپس با docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . از یکی به دیگری آرشیو بگیرید. برای دیتابیس، به جای کپی کردن فایلهای زنده، با ابزار خود دیتابیس dump بگیرید، زیرا کپی فایلی که در حین نوشتن گرفته شده باشد، ممکن است هنگام بازیابی به حالت خراب منجر شود.
آیا دستور docker compose down حجمهای (volumes) من را حذف میکند؟
docker compose down containerها و شبکهها را حذف میکند و named volumeها را دستنخورده باقی میگذارد. docker compose down -v همچنین تمام named volumeهایی که در پروژه تعریف شدهاند را برای همیشه حذف میکند. Bind mountها توسط هیچکدام از این دستورات حذف نمیشوند، زیرا آن دایرکتوری متعلق به host است و نه Docker.