SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor

הגבלת זיכרון ב-Docker Compose למניעת קריסת השרת

למדו כיצד להגדיר mem_limit ו-deploy.resources ב-Docker Compose כדי למנוע ממכולות זוללות זיכרון להפיל את ה-VPS שלכם עם שגיאת exit 137. מדריך מעשי להגדרת משאבים נכונה.

מה עושה הגבלת זיכרון ב-Docker Compose

הגבלת זיכרון ב-Docker Compose היא תקרה קשיחה שה-Linux kernel מציב על ה-cgroup (קבוצת בקרה, רכיב הליבה שמנטר משאבים עבור קבוצת תהליכים) של מכולה בודדת. הגדירו deploy.resources.limits.memory בשירות, והמכולה לעולם לא תוכל לצרוך יותר מהערך שצוין. כאשר היא מנסה לעשות זאת, ה-kernel מסיים תהליך בתוך המכולה, והמכולה לרוב מסתיימת עם קוד 137.

זה קריטי במיוחד ב-VPS, שבו כמות ה-RAM קבועה ואין זיכרון פנוי במארח שניתן לשאול. מכולה אחת עם דליפת זיכרון או שאילתה שגויה תצרוך כל דף זיכרון פנוי בשרת בעל 8GB RAM. ה-kernel יסיים אז את התהליך שהוא מזהה כבעייתי ביותר, שלעיתים קרובות הוא מסד הנתונים או סשן ה-SSH שלכם, ולא המכולה שגרמה לבעיה. הגבלות הופכות קריסה של שרת שלם לתקלה בשירות בודד שמתבצע לו restart.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

החילו את ההגדרה ואשרו שההגבלה פעילה:

docker compose up -d
docker stats --no-stream

העמודה MEM USAGE / LIMIT צריכה להציג ערך הדומה ל-142MiB / 1GiB. אם עמודת ההגבלה מציגה את מלוא ה-RAM של המארח, ההגדרה לא הוחלה, ושאר המדריך לא יסייע עד שהיא תוחל. אם קובץ ה-compose עצמו חדש לכם, יסודות Docker Compose עבור VPS מכסים את מבנה הקובץ שעליו מתבסס תהליך זה.

deploy.resources.limits או mem_limit: מה מהם תקף

קיימים שני כתיבים לאותו רעיון, וזה מה שיוצר בלבול.

mem_limit, mem_reservation, memswap_limit, cpus ו-cpu_shares הם מפתחות ברמה העליונה של השירות, אשר עברו בירושה מפורמטי קובצי Compose ישנים. deploy.resources הגיע מה-schema של Swarm וכעת הוא חלק מ-Compose Specification, הפורמט ש-docker compose קורא כיום.

שניהם עובדים על מארח בודד. Docker Compose, ה-plugin של docker compose, מיישם את deploy.resources.limits ואת deploy.resources.reservations כאשר מריצים את docker compose up, גם ללא אשכול Swarm. ה-plugin נמצא כעת בגרסת v5 (גרסה v5.6.0 הייתה הגרסה היציבה האחרונה שנבדקה בתאריך 2026-10-06). הוא קפץ מ-v2 ישירות ל-v5, תוך דילוג על 3 ו-4 כדי למנוע בלבול עם גרסאות פורמט קובץ ה-Compose הישנות, ואף גרסת v5 לא שינתה את האופן שבו מנוהלות מגבלות משאבים. החלקים בבלוק deploy שרלוונטיים ל-Swarm בלבד הם המפתחות האחרים: mode, placement, update_config ו-endpoint_mode משמעותיים עבור docker stack deploy ומתעלמים מהם ב-docker compose up. לכן, העצה הנפוצה ש-"deploy דורש Swarm" שגויה עבור תת-סעיף ה-resources, וציות לה מותיר את השירותים שלכם ללא כל מגבלה.

בחרו כתיב אחד לכל פרויקט. כתיבת mem_limit: 512m ו-deploy.resources.limits.memory: 1g על אותו שירות יוצרת קובץ שאף אחד לא יכול לקרוא במבט חטוף. במקום לנחש איזה מספר ניצח, שאלו את ה-daemon:

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

ערכי זיכרון הם בבתים, לכן 1g מודפס כ-1073741824. מעבד (CPU) מחושב ב-nano CPUs, לכן 1.5 מודפס כ-1500000000. ערך 0 בכל שדה משמעו שלא הוגדרה מגבלה. מגבלת הזיכרון הקטנה ביותר ש-Docker מקבל היא 6m, ומתחת לערך זה המכולה תסרב לעלות.

מה קורה כאשר מכולה מגיעה למגבלה

המכולה אינה מאטה. היא קורסת.

כאשר תהליך מבקש דף זיכרון וה-cgroup כבר נמצא ב-memory.max שלו, ה-kernel מנסה תחילה לפנות כל מה שניתן בתוך ה-cgroup: מטמון דפים נקיים (clean page cache), ולאחר מכן דפים שניתן להעביר ל-swap. אם פעולת הפינוי אינה משחררת מספיק זיכרון, ה-OOM (out of memory) killer של ה-cgroup בוחר תהליך בתוך המכולה ושולח לו SIGKILL. הריגת ה-PID 1 של המכולה מסיימת את פעולתה. קוד היציאה 137 הוא פשוט 128 ועוד סיגנל 9, לכן 137 הוא סימן מזהה לכל SIGKILL, ולא הוכחה לכך שמדובר ב-OOM כשלעצמו.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 הוא הריגת OOM. false 137 מציין שגורם אחר שלח SIGKILL, והסיבה הנפוצה לכך היא ש-docker compose stop הגיע לתקופת החסד של עשר שניות שלו כיוון שהיישום התעלם מ-SIGTERM. הבחנה זו חוסכת שעות של עבודה, כיוון שאין קשר בין שתי הבעיות.

שני מקומות נוספים מתעדים את האירוע. עקבו אחר ה-daemon בזמן אמת:

docker events --filter event=oom

לאחר מכן קראו את לוג ה-kernel, שהוא התיעוד ששורד אתחול:

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

הריגה של cgroup מדפיסה שורה המתחילה ב-Memory cgroup out of memory: Killed process 24713 (node). שורה ללא הקידומת Memory cgroup היא OOM של המארח (host), מה שאומר שהמכונה עצמה אזלה מ-RAM. זהו הכשל שהמגבלות נועדו למנוע, לכן הופעתו היא סימן לכך שסכום המגבלות שלכם גבוה מדי, או שחלק מהשירותים אינם מוגבלים כלל.

עם restart: unless-stopped, לולאת OOM מוסתרת היטב, כיוון שהשירות נראה פעיל ב-docker compose ps שניה לאחר שקרס. בדקו את עמודת ה-uptime ואת מונה האתחולים, ושלבו את המגבלה עם בדיקת תקינות המדווחת על היישום כלא תקין כדי שמכולה שקורסת שוב ושוב תהיה גלויה גם מבלי שתצטרכו להשגיח עליה.

הקצאה (Reservation) היא המלצה, המגבלה (Limit) היא הכלל

reservations.memory (הגרסה הישנה יותר mem_reservation) היא רף תחתון גמיש. Docker מגדירה זאת כהגבלה רכה המופעלת כאשר ה-daemon מזהה תחרות על משאבים או מחסור בזיכרון במארח. היא לעולם לא תעצור מכולה מלחרוג מעבר לה, והיא לא מבטיחה שהזיכרון יהיה פנוי כאשר המכולה תבקש אותו. היא רק מטה את ה-kernel להעדיף פינוי זיכרון ממכולות שחורגות מההקצאה שלהן תחילה.

לכן, הקצאה אינה מגנה על דבר כשלעצמה. השתמשו בה כדי לסמן שירות שברצונכם שיתועדף בעת עומס, והסתמכו על המגבלה לצורך בטיחות. שמרו על ההקצאה נמוכה מהמגבלה, אחרת המכולה לא תעלה: Docker תדחה את התצורה עם השגיאה Minimum memory limit can not be less than memory reservation limit.

חשבונאות Swap, בכנות

רוב תמונות ה-VPS מגיעות ללא קובץ swap כלל. הריצו את swapon --show ו-free -h. אם סך ה-swap הוא אפס, כל הגדרת swap להלן לא תבצע דבר, ומגבלת הזיכרון שלכם תהיה תקרה של RAM בלבד.

memswap_limit אינו כמות ה-swap. זהו הסכום של הזיכרון וה-swap יחד. עם mem_limit: 1g ו-memswap_limit: 2g, המכולה מקבלת 1GB של RAM ו-1GB של swap. הגדרת שני הערכים כזהים לא תיתן למכולה swap כלל. הגדרת mem_limit והשארת memswap_limit ללא הגדרה מאפשרת למכולה להשתמש ב-swap עד לגודל מגבלת הזיכרון שלה שוב.

Ubuntu 24.04 ו-Debian 13 משתמשות ב-cgroup v2 כברירת מחדל, שם ה-swap הוא מונה נפרד (memory.swap.max) וזה עובד ללא הגדרה נוספת. ההודעה הישנה Your kernel does not support swap limit capabilities מגיעה ממארחים מבוססי cgroup v1 שעלו ללא swapaccount=1. באלו, מגבלת הזיכרון עדיין חלה בעוד שחלק ה-swap מתעלם.

היו כנים לגבי מה ש-swap קונה לכם. הוא הופך OOM kill לאיטי יותר, לא לפחות סביר, כיוון שתהליך דולף ממלא swap באותה שמחה שבה הוא ממלא RAM. בינתיים, מכולה שמבצעת thrashing ל-swap על גבי אחסון VPS משותף מאטה כל שירות אחר על השרת. עבור כל דבר הרגיש לזמן תגובה (latency), מגבלה נכונה ללא swap נכשלת מהר יותר ובאופן צפוי יותר.

מדוע ניצול הזיכרון נראה גרוע מכפי שהוא באמת

הערך MEM USAGE בתוך docker stats כולל את ה-page cache, לכן מכולה שקוראת קבצים גדולים תראה עלייה בשימוש בזיכרון עד להתקרבות למכסה שלה, ושם הוא ייעצר. זהו מצב תקין ואין מדובר בדליפת זיכרון, כיוון שזיכרון מטמון (cache) נקי מוחזר למערכת לפני שה-OOM killer מופעל. שירות כמו שרת מדיה Jellyfin באירוח עצמי ייראה כמי שנמצא באופן קבוע קרוב לתקרת הזיכרון שלו בדיוק מהסיבה הזו.

ניתן לפצל את המספר לזיכרון מטמון ולסט העבודה האמיתי מתוך המכולה:

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon הוא זיכרון אנונימי (anonymous memory), כלומר סט העבודה שלא ניתן לפינוי. file הוא ה-page cache, שניתן לפינוי. הגדירו את המכסה שלכם בהתבסס על anon בתוספת מרווח ביטחון, ולא לפי הסך הכולל. הקובץ memory.events מכריע בוויכוח באופן חד-משמעי: מונה oom_kill הגדול מאפס מעיד על כך שה-kernel הרג תהליך כלשהו במכולה מאז שהיא הופעלה, ומונה max עולה מעיד על כך שהמכולה מוחזקת כרגע בתקרת הזיכרון שלה. שני הפקודות דורשות shell ו-coreutils בתוך ה-image, לכן הן ייכשלו בשימוש ב-distroless או ב-scratch image.

מגבלות גודל בשרת VPS עם 8GB RAM

התחילו מהמארח (host), לא מהיישומים. בשרת VPS עם 8GB, הקצו כ-1GB עבור ה-kernel, ה-daemon של Docker, ‏sshd, ‏journald ומעטפת ה-login שלכם. נותרו לכם בערך 7GB לחלוקה, וסכום המגבלות של כל המכולות צריך להישאר מתחת לרף זה. הקצאת יתר (overcommitting) עובדת היטב עד ליום שבו שני שירותים מגיעים לשיא צריכה בו-זמנית.

חלוקה הגיונית לשרת עם 8GB:

  • Reverse proxy: מגבלה של 128m. זהו תהליך קטן, ומגבלה הדוקה כזו תזהה מיד טעינה מחדש של תצורה שיוצאת משליטה.
  • PostgreSQL: מגבלה של 2g, כאשר shared_buffers מוגדר לכ-512MB בתצורת מסד הנתונים.
  • מכולת יישום: מגבלה של 1g.
  • Background worker: מגבלה של 512m.
  • שירות מדיה או קבצים: מגבלה של 2g, שרובה תשמש כ-page cache.

אל תעתיקו את המספרים הללו לסטאק שלכם. הריצו את השירותים תחת עומס אמיתי למשך יום, עקבו אחר docker stats, קחו את ערך ה-anon המקסימלי לכל מכולה והוסיפו בערך חצי מהערך הזה כמרחב תמרון (headroom). מגבלה הדוקה מדי גרועה יותר מחוסר מגבלה, כיוון שהיא תפיל שירות תקין במהלך עלייה רגילה בתעבורה.

מלכודת אחת ראויה להתייחסות מיוחדת. המגבלה אינה גלויה לרוב סביבות הריצה (runtimes) אלא אם תגדירו זאת במפורש. PostgreSQL יקצה בשמחה shared_buffers ו-work_mem מעבר למגבלת המכולה שלו ויסתיים ב-kill. סביבת JVM ‏(Java virtual machine) זקוקה ל--XX:MaxRAMPercentage=75 כדי לקבוע את גודל ה-heap שלה לפי מגבלת ה-cgroup ולא לפי ה-RAM של המארח. Node.js זקוק ל---max-old-space-size במגה-בייטים, שיוגדר מתחת למגבלת המכולה, אחרת ה-garbage collector יאפשר ל-heap לגדול עד שה-kernel יתערב. המצב ב-Ollama דומה עם פרמטר אחר, כיוון ש-הגדלת num_ctx מגדילה את ה-KV cache במאות מגה-בייטים, והמכולה קורסת באמצע עיבוד של prompt ארוך. ה-cgroup אינו מנהל משא ומתן. הוא פשוט מבצע kill.

מגבלות CPU מתנהגות בצורה שונה לחלוטין

cpus: "1.5" משמעותו 150% מליבה אחת, הנאכף כמכסה של CFS (‏Completely Fair Scheduler). המכולה מקבלת 150ms של זמן מעבד בכל תקופה של 100ms, זמן המחולק בין כל ה-threads שלה. כאשר המכסה מנוצלת, ה-kernel מאלץ את המכולה להמתין לתקופה הבאה.

זהו הניגוד החשוב. מכולה שחורגת ממגבלת ה-memory שלה מסתיימת (killed). מכולה שחורגת ממגבלת ה-CPU שלה עוברת throttling וממשיכה לעבוד, אך לאט יותר. לכן, בטוח להגדיר מגבלת CPU בצורה אגרסיבית, בעוד שמגבלת memory דורשת מרווח ביטחון.

cpu_shares הוא כלי שונה: מדובר במשקל יחסי שמשפיע רק כאשר ה-CPUs נמצאים ברוויה מלאה. שתי מכולות עם shares של 1024 ו-512 יחלקו ליבה עמוסה ביחס של בערך שניים לאחד, אך במערכת פנויה אף אחת מהן לא תוגבל. השתמשו ב-shares כדי לדרג שירותים לפי חשיבות, והשתמשו ב-cpus כאשר אתם זקוקים לתקרה ממשית, למשל כדי למנוע ממשימת קידוד לילית לגזול משאבים משרת ה-web שלכם.

FAQ

האם deploy.resources.limits עובד ללא Docker Swarm?

כן. Docker Compose, באמצעות הפלאגין docker compose (כעת בגרסה v5), מחיל את deploy.resources.limits ואת deploy.resources.reservations כאשר מריצים docker compose up על מארח בודד. ניתן לאמת זאת בעזרת docker inspect --format '{{.HostConfig.Memory}}' <container>, שמציג את המגבלה בבתים ומדפיס 0 כאשר לא הוגדרה מגבלה. המפתחות בתוך deploy שדורשים באמת את Swarm הם mode, placement, update_config ו-endpoint_mode.

מה המשמעות של קוד יציאה 137 ב-Docker Compose?

המשמעות היא שהתהליך הראשי קיבל SIGKILL, שכן 137 הוא 128 ועוד אות 9. הגורם הנפוץ לכך הוא ה-OOM killer של הליבה, אך גם פקיעת זמן כיבוי (shutdown timeout) מפיקה את אותו קוד כאשר יישום מתעלם מ-SIGTERM. הריצו את docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> כדי להבדיל ביניהם. true 137 מעיד על סגירה עקב חריגת זיכרון, בעוד false 137 אינו מעיד על כך.

האם עליי להגדיר mem_limit או deploy.resources.limits.memory?

שניהם עובדים עם docker compose. deploy.resources.limits.memory הוא הפורמט הנוכחי לפי מפרט ה-Compose, והוא הבחירה המועדפת לקובץ חדש. השתמשו ב-mem_limit אם שאר הקובץ שלכם כבר משתמש במפתחות הישנים ברמה העליונה. הגדרת שניהם עבור אותו שירות רק מקשה על קריאת הקובץ, לכן בחרו באחד ואמתו את התוצאה עם docker inspect.

מדוע המכולה שלי נמצאת במגבלת הזיכרון המלאה שלה מבלי להיסגר?

נתון השימוש ב-docker stats כולל את ה-page cache, שהליבה מפנה תחת לחץ במקום להפעיל OOM kill. הריצו את docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat וקראו את הערך anon, המייצג את ה-working set שלא ניתן לשחרר. ערך file גבוה לצד ערך anon נמוך מעידים על מכולה שמבצעת פעולות קלט/פלט בדיסק, ולא על מכולה שעומדת לקרוס.

כמה זיכרון RAM כדאי להשאיר לא מוקצה בשרת VPS עם 8GB?

השאירו כ-1GB עבור הליבה, ה-daemon של Docker, ‏sshd, ‏journald וה-shell שלכם, ושמרו על סכום כל מגבלות המכולות מתחת ל-7GB הנותרים. עקבו אחר ערך ה-anon המקסימלי לכל מכולה תחת עומס אמיתי במשך יום לפני שתקבעו מספרים, והתייחסו לסך הכל כאל תקציב ולא כאל יעד למילוי.