SSD Nodes Learn Hosting plans →
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-10-25

הגבלת זיכרון ב-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 הגיע מסכימת 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-10). הוא קפץ מ-v2 ישר ל-v5, תוך דילוג על 3 ו-4 כדי למנוע בלבול עם גרסאות הפורמט הישנות של Compose, ואף גרסת v5 לא שינתה את האופן שבו מנוהלות מגבלות משאבים. החלקים המכוונים ל-Swarm בבלוק deploy הם המפתחות האחרים, כגון placement, update_config ו-endpoint_mode, המתארים תזמון ופריסות על פני אשכול צמתים עבור docker stack deploy. לכן, העצה הנפוצה ש-"deploy דורש Swarm" אינה נכונה עבור תת-סעיף המשאבים, וציות לה מותיר את השירותים שלכם ללא כל מגבלה.

בחרו כתיב אחד לכל פרויקט. כתיבת mem_limit: 512m ו-deploy.resources.limits.memory: 1g על אותו שירות היא יותר מסתם קשה לקריאה: ה-Compose Specification קובע שכאשר mem_limit מוגדר, הוא חייב להיות עקבי עם limits.memory, והוא אינו מציין איזה ערך גובר במקרה של סתירה. באיזה כתיב שתבחרו, ודאו מה ה-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: תחילה 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 kill. false 137 בדרך כלל אומר שמשהו אחר שלח SIGKILL (תוכנית יכולה גם לצאת עם 137 בכוונה, אך זה נדיר), והסיבה הנפוצה לכך היא ש-docker compose stop הגיע לתקופת החסד של עשר השניות שלו כיוון שהיישום התעלם מ-SIGTERM. ההבחנה הזו חוסכת שעות של עבודה, כיוון שאין קשר בין שתי הבעיות.

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

docker events --filter event=oom

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

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

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

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

הקצאה (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 נכשלת מהר יותר ובאופן צפוי יותר.

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

במערכת Linux, הנתון MEM USAGE בתוך docker stats מחסיר רק את ה-page cache הלא פעיל (inactive_file ב-cgroup v2), לכן מטמון שנעשה בו שימוש לאחרונה עדיין נספר. מכולה שקוראת קבצים גדולים תטפס לכיוון המגבלה שלה ותישאר שם. זהו מצב תקין ואין מדובר בדליפת זיכרון, כיוון שמטמון נקי (clean 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 הוא זיכרון אנונימי, סט העבודה שלא ניתן למחוק. file הוא ה-page cache, שניתן למחיקה. קבעו את המגבלה שלכם לפי anon בתוספת מרווח ביטחון, ולא לפי הסך הכולל. הקובץ memory.events מכריע את הוויכוח באופן סופי: מונה oom_kill הגדול מאפס אומר שה-kernel הרג תהליך כלשהו במכולה זו מאז שהיא עלתה, ומונה max עולה אומר שהמכולה מוחזקת כרגע בתקרת הזיכרון שלה. שני הפקודות דורשות shell ו-coreutils בתוך ה-image, לכן הן ייכשלו ב-distroless או ב-scratch image.

מגבלות גודל בשרת VPS עם 8GB זיכרון

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

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

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

אל תעתיקו את המספרים הללו ל-stack שלכם כפי שהם. הריצו את השירותים תחת עומס אמיתי למשך יום, עקבו אחר docker stats, קחו את ערך ה-anon המקסימלי לכל container והוסיפו לו בערך חצי מהערך כמרחב נשימה. מגבלה הדוקה מדי גרועה יותר מחוסר מגבלה, כיוון שהיא תגרום להפסקת שירות תקין במהלך עלייה רגילה בתעבורה.

מלכודת אחת ראויה להתייחסות מיוחדת. סביבות הרצה (runtimes) מסוימות מתעלמות מהמגבלה אלא אם תגדירו להן זאת, ואלו שכן קוראות אותה עשויות שלא להקצות זיכרון כפי שציפיתם. PostgreSQL יקצה בשמחה את shared_buffers ו-work_mem מעבר למגבלת ה-container שלו ויסתיים ב-kill. סביבת JVM (Java virtual machine) מודרנית מזהה את מגבלת ה-cgroup כברירת מחדל, אך גרסאות HotSpot כמו OpenJDK מקצות ל-heap רק 25 אחוזים ממנה, לכן -XX:MaxRAMPercentage=75 היא הדרך המקובלת לאפשר ל-heap לנצל חלק גדול יותר מהמגבלה תוך השארת מקום לזיכרון מחוץ ל-heap. Node.js מקצה את ה-heap כברירת מחדל לפי מגבלת ה-cgroup מאז גרסה 12, בערך בחצי ממגבלת ה-container ועם תקרה של כ-2GB, לכן הגדירו --max-old-space-size במגה-בייט רק כאשר אתם מעוניינים בתקרה שונה, והקפידו שהיא תהיה נמוכה ממגבלת ה-container כדי שפעולת ה-garbage collector תתבצע לפני שה-kernel יתערב. המצב ב-Ollama דומה עם כפתור כוונון שונה, כיוון ש-הגדלת num_ctx מגדילה את ה-KV cache במאות מגה-בייטים וה-container קורס באמצע עיבוד של 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 מיועדים לקלאסטר, כגון placement, update_config ו-endpoint_mode.

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

המשמעות היא כמעט תמיד שהתהליך הראשי קיבל SIGKILL, שכן 137 הוא 128 ועוד סיגנל 9 (תוכנית יכולה גם לצאת עם 137 באופן מכוון, אך זה נדיר). ה-OOM killer של ה-kernel הוא הגורם הנפוץ, אך גם 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 אם שאר הקובץ שלכם כבר משתמש במפתחות ברמה העליונה הישנים. מפרט ה-Compose דורש עקביות בין השניים כאשר שניהם מוגדרים, לכן בחרו אחד ואמתו את התוצאה עם docker inspect.

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

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

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

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

האם המאמר הזה היה מועיל?