volumeهای Docker Compose: bind یا نامگذاریشده؟
تفاوت bind mount و volume نامگذاریشده در Docker Compose را برای پیکربندی و داده ببینید و خطاهای مجوز فایل، بازرسی، پشتیبانگیری و مهاجرت را مدیریت کنید.
اتصال پوشه یا volume نامگذاریشده: پاسخ کوتاه
volumeهای Docker Compose دو نوع دارند و انتخاب بین آنها به این بستگی دارد که چه کسی مالک فایلها است. برای فایلهایی که خودتان مینویسید و میخوانید، از اتصال پوشه استفاده کنید؛ مانند فایلهای پیکربندی، قالبها و سایتهای ایستا. برای دادههایی که برنامه مالک آنها است، از volume نامگذاریشده استفاده کنید؛ مانند فایلهای پایگاه داده، شاخصهای جستوجو و رسانههای بارگذاریشده. اتصال پوشه به مسیری در میزبان اشاره میکند که میتوانید آن را در یک ویرایشگر باز کنید. volume نامگذاریشده فضای ذخیرهسازیای است که Docker آن را برای شما ایجاد و مدیریت میکند و از طریق Docker به آن دسترسی دارید.
هر دو نوع، زیر کلید یکسان volumes: داخل یک سرویس قرار میگیرند؛ به همین دلیل با یکدیگر اشتباه گرفته میشوند. تفاوت در سمت چپ دونقطه است. اگر سمت چپ با . یا / شروع شود، یک مسیر میزبان است؛ بنابراین اتصال پوشه محسوب میشود. هر مقدار دیگری یک نام است؛ بنابراین 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 آن را بهصورت فقطخواندنی mount میکند؛ این مقدار پیشفرض مناسبی برای پیکربندیای است که container نباید هرگز آن را بازنویسی کند. اگر ورودی سطح بالای volumes: را فراموش کنید، Compose با service "db" refers to undefined volume pgdata متوقف میشود.
آن را راهاندازی کنید و مواردی را که Docker ایجاد کرده است فهرست کنید:
docker compose up -d
docker volume lsنام volume، pgdata نیست. نام آن <project>_pgdata است؛ نام پروژه بهصورت پیشفرض از نام directory حاوی فایل compose گرفته میشود. برای directory با نام myapp، مقدار myapp_pgdata ایجاد میشود. این موضوع مهم است، زیرا تغییر نام directory یک volume خالی جدید ایجاد میکند و به نظر میرسد برنامه دادههای خود را از دست داده است. چنین اتفاقی نیفتاده است: volume قدیمی همچنان با docker volume ls فهرست میشود. نام را با name: در فایل compose ثابت کنید، یا اگر ممکن است directory جابهجا شود، COMPOSE_PROJECT_NAME را تنظیم کنید. تنظیماتی از این نوع باید در کنار فایلهای محیطی و secrets در Compose قرار بگیرند.
چرا خطاهای مجوز فقط در bind mount رخ میدهند
این مهمترین تفاوت عملی است و از یک قاعده ناشی میشود: یک named volume که در نخستین استفاده خالی است، از image مقداردهی اولیه میشود؛ اما bind mount هرگز چنین کاری نمیکند.
وقتی Docker یک named volume خالی را روی دایرکتوریای mount میکند که از قبل در image محتوا دارد، آن محتوا را با مالکیت و modeهای تعیینشده در image داخل volume کپی میکند. image رسمی Postgres، /var/lib/postgresql/data را متعلق به کاربر postgres خودش ارائه میکند. بنابراین volume نیز با همان id عددی مالکیت پیدا میکند و پایگاه داده شروع به کار میکند.
bind mount برعکس عمل میکند. هر چیزی که روی host وجود داشته باشد، همراه با مالکیت آن، چیزی است که container میبیند و محتوای image در آن مسیر پنهان میشود. اگر دایرکتوری روی host وجود نداشته باشد، Docker daemon آن را ایجاد میکند و daemon با root اجرا میشود؛ بنابراین دایرکتوری متعلق به root:root خواهد بود. در نتیجه، فرایندی که داخل container با کاربر غیرroot اجرا میشود، نمیتواند در آن بنویسد:
PermissionError: [Errno 13] Permission denied: '/data/app.db'راهحل این است که idهای عددی را یکسان کنید. مالکیت در bind mount بر اساس user id عددی مقایسه میشود، نه نام کاربر؛ زیرا 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 ./datadocker compose exec web id نشان میدهد فرایند container عملاً با چه uidای اجرا میشود. دایرکتوری روی host را با همان عدد مطابقت دهید، یا با استفاده از user: "1000:1000" در سرویس، container را روی uid خودتان ثابت کنید. ثابتکردن user: برای برنامهای که خودتان نوشتهاید، روش تمیزتری است. برای imageای که خودتان ننوشتهاید، تغییر مالکیت دایرکتوری روی host ایمنتر است؛ زیرا بعضی imageها entrypoint را ابتدا با root اجرا میکنند، سپس سطح دسترسی را کاهش میدهند و انتظار دارند مالکیتهای مشخصی در مسیرهای زیرین وجود داشته باشد.
دو نکته مهم دیگر نیز وجود دارد. در Fedora، RHEL و سیستمهای دیگری که SELinux (security-enhanced Linux) در حالت enforcing فعال است، bind mount تا زمانی که برچسبگذاری مجدد نشود، رد میشود. بنابراین برای مسیری که بین containerها به اشتراک گذاشته میشود، :z را اضافه کنید و برای مسیری که فقط یک container باید از آن استفاده کند، :Z را اضافه کنید؛ این گزینه بهشکل - ./data:/data:Z نوشته میشود. همچنین bind mount کردن یک فایل منفرد، بهجای یک دایرکتوری، زمانی مشکلساز میشود که ویرایشگر فایل را جایگزین کند و در همان inode ننویسد؛ زیرا mount به inode اصلی وابسته میماند. در این حالت، container تا زمان restart شدن همچنان محتوای قدیمی را میبیند. اگر فایل مرتب ویرایش میشود، دایرکتوری والد آن را mount کنید.
عملکرد: تفاوت واقعی کجاست
در یک سرور Linux، هر دو نوع از مسیر یکسانی در kernel عبور میکنند؛ بنابراین تفاوت توان عملیاتی آنقدر کم است که نباید بر اساس آن انتخاب کنید. volumeهای نامگذاریشده که از driver پیشفرض local استفاده میکنند، روی همان filesystem مربوط به Docker و در مسیر /var/lib/docker/volumes/ قرار دارند. bind mount در مسیری قرار میگیرد که شما تعیین کردهاید.
این تفاوت در Docker Desktop برای macOS و Windows آشکار میشود؛ زیرا containerها داخل یک ماشین مجازی اجرا میشوند. در آنجا، bind mount از طریق یک لایه اشتراک فایل، از filesystem میزبان وارد آن ماشین مجازی میشود. workloadهایی که عملیات زیادی روی فایلهای کوچک انجام میدهند، مانند درخت dependency مربوط به Node.js یا cache مربوط به یک framework در PHP، بهطور محسوسی کند میشوند. volumeهای نامگذاریشده داخل ماشین مجازی باقی میمانند و این هزینه را ندارند. به همین دلیل، بسیاری از فایلهای compose برای محیط توسعه، دایرکتوری source را بهصورت bind mount متصل میکنند، اما روی node_modules یک volume نامگذاریشده تعریف میکنند.
تفاوت واقعی دیگر، محل ذخیرهشدن دادهها است. bind mount به /mnt/backup دادهها را روی همان disk ذخیره میکند. volume نامگذاریشده روی filesystemای قرار میگیرد که /var/lib/docker را در خود دارد؛ این filesystem در یک VPS معمولاً همان root disk است. اگر یک database داخل volume نامگذاریشده رشد کند، همان disکی را پر میکند که logهای سیستم روی آن قرار دارند. پیش از تبدیلشدن این وضعیت به یک incident، آن را بررسی کنید:
docker system df -v
df -h /var/lib/dockerdocker system df -v همه volumeها را همراه با اندازه آنها فهرست میکند و volumeهایی را که دیگر هیچ containerای به آنها ارجاع نمیدهد، مشخص میکند.
بررسی یک volume نامگذاریشده
یک volume نامگذاریشده جعبهای سیاه نیست. از Docker بپرسید این volume کجا قرار دارد:
docker volume inspect myapp_pgdataفیلد Mountpoint مسیر واقعی روی میزبان را نشان میدهد که معمولاً /var/lib/docker/volumes/myapp_pgdata/_data است. میتوانید آن را با sudo ls بخوانید و برای یک بررسی سریع از آن استفاده کنید. این مسیر را محل ویرایش فایلها در نظر نگیرید. نوشتن در آن بهعنوان root مشکل مالکیتی توضیحدادهشده در بالا را دوباره ایجاد میکند. همچنین این مسیر جزئی از جزئیات driver مربوط به local است و سایر volume driverها چنین مسیری ندارند.
روش ایمن برای مشاهده محتویات، استفاده از یک container موقتی است که volume را mount میکند:
docker run --rm -v myapp_pgdata:/vol alpine ls -la /volاین روش با هر driver کار میکند، همان مجوزهایی را میبیند که container واقعی میبیند و بهدلیل --rm چیزی باقی نمیگذارد.
پشتیبانگیری از هر نوع
bind mount یک دایرکتوری معمولی است؛ بنابراین هر ابزار پشتیبانگیری در سطح فایل از آن پشتیبانی میکند. مقصد پشتیبانگیری را مسیر میزبان قرار دهید و کار تمام است. volume نامگذاریشده به یک مرحله اضافی نیاز دارد، زیرا ابزار باید به داخل آن دسترسی پیدا کند. volume و یک دایرکتوری میزبان را در همان 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 /datatar هنگام اجرا بهعنوان root درون container، مالکیت عددی را حفظ میکند. همین موضوع باعث میشود volume بازیابیشده برای application قابل استفاده بماند.
یک هشدار درباره هر دو نوع صدق میکند. کپیکردن فایلهای database هنگام اجرای database، آرشیوی از دادههایی ایجاد میکند که در حال تغییر هستند و ممکن است بازیابی آن به وضعیت خراب منجر شود. ابتدا service را متوقف کنید یا با ابزار خود database، دادهها را dump کنید؛ مانند docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. این کار یک فایل ساده ایجاد میکند که سپس میتوانید آن را همراه با compose files، در یک روال پشتیبانگیری رمزگذاریشده با 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 مالکیت، حالتهای دسترسی و زمانهای ثبتشده را حفظ میکند؛ بنابراین کاربر کانتینری که میتوانست دایرکتوری قبلی را بخواند، همچنان میتواند volume جدید را بخواند. سپس سرویس را طوری تغییر دهید که از pgdata:/var/lib/postgresql/data استفاده کند، pgdata را به بلوک سطحبالای volumes: اضافه کنید، docker compose up -d را اجرا کنید و پیش از حذف دایرکتوری قبلی، لاگهای برنامه را بخوانید. برای انجام این کار در جهت معکوس، از همان دستور استفاده کنید و جای /from و /to را عوض کنید.
هنگام آزمایش، یک نکته را به خاطر داشته باشید. docker compose down به volumeهای نامگذاریشده دست نمیزند، اما docker compose down -v همه volumeهای نامگذاریشدهای را که پروژه تعریف کرده است حذف میکند و امکان بازگردانی وجود ندارد. یک bind mount از هر دو عملیات جان سالم به در میبرد، زیرا Docker هرگز مالک آن دایرکتوری نبوده است. اگر دستورات مدیریت چرخه عمر هنوز برایتان جدید هستند، راهنمای مبانی Docker Compose برای یک VPS مراحل آنها را توضیح میدهد.
انتخاب، سرویس به سرویس
مشخص کنید چه چیزی فایل را مینویسد. پیکربندیای که آن را در ویرایشگر متن ویرایش میکنید و در git ثبت میکنید، باید در یک bind mount قرار بگیرد و با :ro متصل شود، زیرا میخواهید قابل مشاهده و نسخهبندیشده باشد. دادههای وضعیت برنامه را که هرگز بهصورت دستی باز نمیکنید، در یک named volume قرار دهید، زیرا Docker مجوزها را بهدرستی ایجاد میکند و دادهها به یک مسیر در میزبان وابسته نیستند.
حالت ترکیبی مربوط به رسانه است. برنامه کتابخانه عکس را مینویسد، اما مدیریت آن نیز با شماست و این کتابخانه اغلب آنقدر بزرگ است که به یک دیسک مشخص نیاز دارد. آن را به مسیری روی همان دیسک bind mount کنید و مالکیت را یکبار بهصورت صریح تنظیم کنید. این همان الگویی است که بیشتر stackهای self-hosted انتخاب میکنند: برای پایگاههای داده و cacheها از named volume و برای پیکربندی و دایرکتوری بزرگی که برایتان اهمیت دارد از bind mount استفاده کنید.
FAQ
تفاوت بین bind mount و named volume چیست؟
یک bind mount مسیری را از میزبان به داخل کانتینر نگاشت میکند؛ بنابراین هر دو طرف همان دایرکتوری را میبینند و میتوانید آن را با ابزارهای معمول ویرایش کنید. یک named volume فضای ذخیرهسازیای است که Docker آن را ایجاد و مدیریت میکند. این فضا با نام ارجاع داده میشود و در بلوک سطح بالای volumes: اعلام میشود. تفاوت عملی در نحوه مالکیت است: از bind mount برای پیکربندیای استفاده کنید که شما نگهداری میکنید و از named volume برای دادهای که برنامه نگهداری میکند.
چرا با bind mount خطای "permission denied" میگیرم، اما با named volume نه؟
یک named volume خالی از image مقداردهی اولیه میشود؛ بنابراین مالکیتی را که image تعیین کرده است به ارث میبرد و کاربر کانتینر میتواند در آن بنویسد. bind mount دایرکتوری میزبان را دقیقاً همانطور که هست نمایش میدهد. اگر Docker مجبور شده باشد آن دایرکتوری را ایجاد کند، مالکیت آن را به root داده است. برای مشاهده شناسه عددی مورد استفاده کانتینر، docker compose exec <service> id را اجرا کنید. سپس sudo chown -R <uid>:<gid> را روی دایرکتوری میزبان اجرا کنید، یا user: "1000:1000" را روی service تنظیم کنید.
Docker named volumeها را روی دیسک کجا ذخیره میکند؟
با driver پیشفرض local، این volumeها در مسیر /var/lib/docker/volumes/<volume>/_data قرار دارند و docker volume inspect <volume> مقدار دقیق Mountpoint را چاپ میکند. اگر لازم است موردی را بررسی کنید، این مقدار را بخوانید؛ اما فقط از طریق یک کانتینر در آن بنویسید، زیرا ویرایش بهعنوان root روی میزبان، مالکیت را به شکلی تغییر میدهد که کانتینر انتظار آن را ندارد.
چگونه از یک named volume نسخه پشتیبان تهیه کنم؟
یک کانتینر کوتاهعمر اجرا کنید و volume و یک دایرکتوری میزبان را هر دو در آن mount کنید. سپس با docker run --rm -v myvol:/data:ro -v "$PWD":/backup alpine tar czf /backup/myvol.tar.gz -C /data . دادهها را از یکی به دیگری آرشیو کنید. برای پایگاه داده، بهجای کپیکردن فایلهای در حال استفاده، با ابزار اختصاصی همان پایگاه داده dump بگیرید؛ زیرا کپی فایل هنگام انجام عملیات نوشتن ممکن است در زمان بازیابی به وضعیت خراب منجر شود.
آیا docker compose down، volumeهای من را حذف میکند؟
docker compose down کانتینرها و networkها را حذف میکند و named volumeها را در جای خود باقی میگذارد. docker compose down -v همچنین همه named volumeهایی را که پروژه اعلام کرده است بهطور دائمی حذف میکند. هیچیک از این دو command، bind mountها را حذف نمیکند، زیرا آن دایرکتوری به میزبان تعلق دارد، نه به Docker.