Docker Compose: בחירה בין bind mount ל-named volume
מה ההבדל בין bind mount ל-named volume ב-Docker Compose? מדריך מעשי לבחירת סוג האחסון הנכון עבור קבצי הגדרות או בסיסי נתונים, כולל פתרון שגיאת volume not found וניהול הרשאות.
Bind mount או named volume: התשובה הקצרה
ב-Docker Compose קיימים שני סוגים של volumes, והבחירה ביניהם תלויה בשאלה מי הבעלים של הקבצים. השתמשו ב-bind mount עבור קבצים שאתם קוראים ועורכים בעצמכם, כגון קובצי הגדרות, תבניות (templates) ואתרים סטטיים. השתמשו ב-named volume עבור נתונים שהיישום מנהל, כגון קובצי מסד נתונים, אינדקסים של חיפוש ומדיה שהועלתה. ה-bind mount מצביע על נתיב במערכת המארחת (host) שניתן לפתוח בעורך טקסט. ה-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
זהו ההבדל המעשי המשמעותי ביותר, והוא נובע מכלל אחד: volume בעל שם שריק בשימוש הראשון מאותחל מתוך ה-image, בעוד ש-bind mount לעולם אינו עובר תהליך כזה.
כאשר Docker מבצע mount ל-volume ריק בעל שם על גבי תיקייה שכבר מכילה תוכן ב-image, הוא מעתיק את התוכן הזה לתוך ה-volume, עם הבעלות וההרשאות שהוגדרו ב-image. ה-image הרשמי של Postgres מספק את /var/lib/postgresql/data בבעלות המשתמש postgres שלו, לכן ה-volume מקבל את אותו מזהה מספרי (numeric id) והמסד נתונים עולה.
ב-bind mount המצב הפוך. מה שקיים במארח (host) הוא מה שהמכולה רואה, כולל הבעלות, והתוכן של ה-image בנתיב זה מוסתר. אם תיקיית המארח אינה קיימת, ה-daemon של Docker יוצר אותה, ומכיוון שה-daemon רץ כ-root, התיקייה תהיה בבעלות root:root. תהליך במכולה שרץ כמשתמש שאינו root לא יוכל לכתוב אליה:
PermissionError: [Errno 13] Permission denied: '/data/app.db'הפתרון הוא התאמת המספרים. הבעלות ב-bind mount נבדקת לפי מזהה משתמש מספרי (uid), לא לפי שם, כיוון שלמכולה יש /etc/passwd משלה. משתמש בשם app בתוך המכולה אינו אומר דבר למארח. 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 שבו תהליך המכולה רץ בפועל. התאימו את תיקיית המארח למספר זה, או קבעו את המכולה למספר שלכם באמצעות user: "1000:1000" בהגדרת השירות. קביעת user: היא נקייה יותר עבור יישום שכתבתם בעצמכם. שינוי בעלות (chown) לתיקיית המארח בטוח יותר עבור image שלא אתם כתבתם, כיוון שחלק מה-images מריצים entrypoint כ-root, מוותרים על הרשאות, ומצפים לבעלות ספציפית בנתיב.
כדאי להכיר עוד שני מכשולים. ב-Fedora, ב-RHEL ובמערכות אחרות שבהן SELinux פעיל, ה-bind mount ייחסם עד לשינוי ה-label שלו. לכן, הוסיפו :z עבור נתיב משותף בין מכולות, או :Z עבור נתיב שרק מכולה אחת אמורה להשתמש בו, בכתיב - ./data:/data:Z. בנוסף, bind mount של קובץ בודד, במקום תיקייה, עלול להישבר כאשר עורך טקסט מחליף את הקובץ במקום לכתוב עליו (in-place), כיוון שה-mount עוקב אחרי ה-inode המקורי. המכולה תמשיך לראות את התוכן הישן עד שתבצעו לה restart. בצעו mount לתיקיית האב אם הקובץ נערך בתדירות גבוהה.
ביצועים: היכן קיים הפער בפועל
בשרת Linux, שני הסוגים עוברים דרך אותו נתיב ב־kernel, לכן הבדל התעבורה זניח מכדי להוות שיקול בבחירה. Named volumes המשתמשים בדרייבר ברירת המחדל local נמצאים על אותה מערכת קבצים של שאר Docker, תחת /var/lib/docker/volumes/, בעוד ש־bind mount נמצא בכל מקום אליו הפנית אותו.
הפער מופיע ב־Docker Desktop עבור macOS ו־Windows, שם המכולות רצות בתוך מכונה וירטואלית. שם, bind mount חוצה את מערכת הקבצים של המארח אל תוך המכונה הווירטואלית דרך שכבת שיתוף קבצים, ועומסי עבודה עם פעולות רבות על קבצים קטנים, כמו עץ תלויות של Node.js או מטמון של framework ב־PHP, מואטים באופן מורגש. Named volumes נשארים בתוך המכונה הווירטואלית ואינם סופגים את העלות הזו. זו הסיבה שקובצי compose רבים לפיתוח מבצעים bind-mount לספריית המקור אך מצהירים על named volume עבור node_modules.
ההבדל הממשי הנוסף הוא היכן נשמרים הבתים. bind mount ל־/mnt/backup מציב נתונים על כונן זה. named volume ממוקם על כל מערכת קבצים שמחזיקה את /var/lib/docker, שבדרך כלל ב־VPS היא כונן ה־root. מסד נתונים שגדל בתוך named volume ממלא את אותו כונן שבו נמצאים לוגי המערכת שלכם. בדקו זאת לפני שזה הופך לתקרית:
docker system df -v
df -h /var/lib/dockerdocker system df -v מפרט כל volume עם הגודל שלו, ומסמן את אלו שאף מכולה כבר לא מפנה אליהם.
בדיקת נפח (volume) בעל שם
נפח בעל שם אינו קופסה שחורה. ניתן לשאול את 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 דורש שלב נוסף, כיוון שהכלי חייב לגשת לתוכו. יש לעגן (mount) את ה-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 /datatar שומר על הבעלות המספרית (numeric ownership) כאשר הוא רץ כ-root בתוך המכולה, וזה מה שמאפשר ליישום להשתמש ב-volume המשוחזר.
אזהרה אחת תקפה לשני הסוגים. העתקת קבצי מסד נתונים בזמן שהמסד פעיל יוצרת ארכיון של נתונים משתנים, מה שעלול להוביל לשחזור במצב פגום. יש לעצור את השירות תחילה, או לבצע dump באמצעות הכלי הייעודי של מסד הנתונים, כפי שמופיע ב-docker compose exec -T db pg_dump -U postgres appdb > appdb.sql. פעולה זו מייצרת קובץ פשוט שניתן לכלול בתוך שגרת גיבוי מוצפנת של restic לצד קובצי ה-compose שלכם.
העברת bind mount לנפח (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 שומר על הבעלות, ההרשאות וחותמות הזמן, כך שמשתמש המכולה שהיה מסוגל לקרוא את הספרייה הישנה יוכל לקרוא גם את הנפח החדש. לאחר מכן, שנו את השירות כך שישתמש ב-pgdata:/var/lib/postgresql/data, הוסיפו את pgdata לבלוק ה-volumes: ברמה העליונה, הריצו את docker compose up -d, וקראו את הלוגים של היישום לפני מחיקת הספרייה הישנה. ביצוע הפעולה בכיוון ההפוך משתמש באותה פקודה כאשר /from ו-/to מוחלפים ביניהם.
זכרו דבר אחד בזמן הבדיקות. docker compose down מותיר נפחים בעלי שם ללא שינוי, אך docker compose down -v מוחק כל נפח בעל שם שמוגדר בפרויקט, ואין אפשרות לבטל פעולה זו. bind mount שורד את שתי הפקודות, כיוון ש-Docker מעולם לא היה הבעלים של אותה ספרייה. אם פקודות מחזור החיים עדיין חדשות לכם, ה-מדריך יסודות Docker Compose עבור VPS סוקר אותן בפירוט.
בחירה, שירות אחר שירות
שאלו את עצמכם מי כותב את הקובץ. הגדרות שאתם עורכים בעורך טקסט ומבצעים להן commit ל-git שייכות ל-bind mount, המותקן ב-:ro, כיוון שאתם רוצים שהן יהיו גלויות ומנוהלות בגרסאות. מצב (state) של יישום שאינכם פותחים ידנית שייך ל-named volume, כיוון ש-Docker מגדיר את ההרשאות בצורה נכונה והנתונים אינם תלויים בנתיב על המארח.
המקרה המעורב הוא מדיה. ספריית תמונות נכתבת על ידי היישום אך מנוהלת גם על ידכם, ולעיתים קרובות היא גדולה מספיק כדי להצדיק דיסק ייעודי. בצעו לה bind mount לנתיב באותו דיסק, והגדירו את הבעלות עליו באופן יזום פעם אחת. זהו הדפוס שרוב מערכי ה-self-hosted מאמצים: named volumes עבור מסדי נתונים ו-caches, ו-bind mounts עבור הגדרות ועבור הספריות הגדולות שחשובות לכם. שירות תמיכה כמו Chatwoot הרץ על VPS מגיע בדיוק לנקודה זו, עם Postgres ב-named volume וקבצים מצורפים שהועלו בנתיב שניתן להפנות אליו כלי גיבוי.
FAQ
מה ההבדל בין bind mount לבין named volume?
bind mount ממפה נתיב במערכת המארחת (host) לתוך המכולה, כך ששני הצדדים רואים את אותה ספרייה וניתן לערוך אותה בכלים סטנדרטיים. named volume הוא אמצעי אחסון ש-Docker יוצר ומנהל, אליו מתייחסים לפי שם והוא מוצהר בבלוק ה-volumes: ברמה העליונה. ההבדל המעשי הוא בבעלות: השתמשו ב-bind mount עבור קובצי הגדרה שאתם מתחזקים, וב-named volume עבור נתונים שהיישום מתחזק.
מדוע אני מקבל שגיאת "permission denied" ב-bind mount אך לא ב-named volume?
named volume ריק מאותחל מתוך ה-image, לכן הוא יורש את הבעלות שהוגדרה ב-image והמשתמש בתוך המכולה יכול לכתוב אליו. bind mount מציג את ספריית המארח בדיוק כפי שהיא, ואם Docker נדרש ליצור את הספרייה הזו, הוא מגדיר את הבעלות עליה כ-root. הריצו את docker compose exec <service> id כדי לראות את המזהה המספרי (UID) שבו המכולה משתמשת, לאחר מכן בצעו sudo chown -R <uid>:<gid> לספרייה במארח, או הגדירו user: "1000:1000" בשירות.
היכן Docker מאחסן named volumes על הדיסק?
עם ה-driver המוגדר כברירת מחדל 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 .. עבור מסד נתונים, בצעו dump באמצעות הכלי הייעודי של מסד הנתונים במקום להעתיק קבצים חיים, שכן העתקת קבצים בזמן כתיבה עלולה להוביל לשחזור במצב פגום.
האם הפקודה docker compose down מוחקת את ה-volumes שלי?
docker compose down מסירה מכולות ורשתות ומשאירה את ה-named volumes במקומם. docker compose down -v מוחקת גם את כל ה-named volumes שהפרויקט מצהיר עליהם, לצמיתות. bind mounts לעולם אינם מוסרים על ידי אף אחת מהפקודות הללו, כיוון שהספרייה שייכת למערכת המארחת ולא ל-Docker.