تفاوت 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 نیز با همان شناسه عددی (numeric 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 است.
id -u
mkdir -p ./data
docker compose exec web id
sudo chown -R 1000:1000 ./dataدستور docker compose exec web id uid واقعی که پردازش container با آن اجرا میشود را نمایش میدهد. دایرکتوری host را با آن عدد هماهنگ کنید، یا container را با استفاده از user: "1000:1000" در سرویس، روی عدد خودتان قفل (pin) کنید. قفل کردن با user: برای برنامهای که خودتان نوشتهاید تمیزتر است. تغییر مالکیت (chown) دایرکتوری host برای imageهایی که خودتان ننوشتهاید ایمنتر است، زیرا برخی imageها entrypoint را با root شروع میکنند، سپس سطح دسترسی را کاهش میدهند و انتظار دارند مالکیت خاصی در زیرمجموعه مسیر وجود داشته باشد.
دو تله دیگر نیز وجود دارد که دانستن آنها مفید است. در Fedora، RHEL و سایر سیستمهایی که SELinux (امنیت پیشرفته لینوکس) در آنها فعال است، bind mount تا زمانی که برچسبگذاری مجدد نشود، مسدود میگردد؛ بنابراین برای مسیری که بین چند 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 مجوزها را بهدرستی تنظیم میکند و دادهها به مسیر میزبان وابسته نیستند.
حالت ترکیبی، رسانهها هستند. یک کتابخانه عکس توسط برنامه نوشته میشود اما توسط شما مدیریت میگردد و اغلب آنقدر بزرگ است که بخواهید روی یک دیسک خاص باشد. آن را به مسیری روی آن دیسک bind mount کنید و مالکیت آن را یکبار بهطور آگاهانه تنظیم کنید. این الگویی است که اکثر پشتههای self-hosted بر آن توافق دارند: named volume برای پایگاههای داده و کشها، و bind mount برای پیکربندی و دایرکتوریهای بزرگی که برایتان اهمیت دارند. یک میز پشتیبانی مانند اجرای Chatwoot روی یک VPS دقیقاً به همین نقطه میرسد؛ با Postgres در یک named volume و فایلهای پیوستشده در مسیری که میتوانید برای آن یک backup تعریف کنید.
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 میتواند در آن بنویسد. یک bind mount دایرکتوری host را دقیقاً همانطور که هست نمایش میدهد و اگر Docker مجبور به ایجاد آن دایرکتوری شده باشد، مالکیت آن را به root اختصاص میدهد. دستور docker compose exec <service> id را اجرا کنید تا شناسه عددی مورد استفاده توسط container را ببینید، سپس sudo chown -R <uid>:<gid> را روی دایرکتوری host اعمال کنید یا user: "1000:1000" را در سرویس تنظیم نمایید.
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.