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

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

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

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

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

זה קריטי במיוחד ב-VPS, שבו ה-RAM קבוע ואין זיכרון מארח פנוי לשאילה. מכולה אחת עם דליפת זיכרון או שאילתה שגויה תצרוך כל דף זיכרון פנוי בשרת של 8GB. ה-kernel יסיים אז את התהליך שהוא מזהה כבעייתי ביותר, שלעיתים קרובות הוא מסד נתונים או ה-SSH session שלכם, במקום את המכולה שגרמה לבעיה. הגבלות הופכות קריסה של שרת שלם לכשל של שירות בודד שמתבצע לו 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, שהוא הפורמט ש-docker compose קורא כיום.

שניהם עובדים על מארח בודד. Compose V2, תוסף ה-docker compose, מחיל את deploy.resources.limits ו-deploy.resources.reservations כאשר מריצים את docker compose up, גם ללא אשכול Swarm. החלקים בבלוק 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: מטמון דפים נקי, ולאחר מכן דפים שניתן להעביר ל-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, לכן מכולה שקוראת קבצים גדולים תראה עלייה בשימוש בזיכרון עד להתקרבות למכסה שלה. זהו מצב תקין ואינו מעיד על דליפת זיכרון, שכן ה-kernel מפנה זיכרון מטמון (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 זיכרון

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

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

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

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

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

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

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

FAQ

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

כן. גרסה V2 של Compose מחילה את 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 המקסימלי לכל קונטיינר תחת עומס אמיתי במשך יום לפני שתתחייבו למספרים, והתייחסו לסך הכל כאל תקציב ולא כיעד למילוי.