SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor · به‌روزرسانی شده 2026-10-20

محدود کردن رم و CPU در Docker Compose برای جلوگیری از OOM

با تنظیم deploy.resources در Docker Compose از کرش کردن سرور جلوگیری کنید. یاد بگیرید چگونه با اعمال محدودیت memory و cpu از خطای 137 و مصرف بی‌رویه منابع توسط کانتینرها جلوگیری کنید.

محدودیت حافظه در Docker Compose چه کاری انجام می‌دهد

محدودیت حافظه در Docker Compose یک سقف سخت‌گیرانه است که هسته لینوکس روی cgroup (گروه کنترل؛ قابلیتی در هسته که منابع را برای مجموعه‌ای از پردازش‌ها اندازه‌گیری می‌کند) یک کانتینر اعمال می‌کند. مقدار deploy.resources.limits.memory را برای یک سرویس تنظیم کنید تا آن کانتینر هرگز نتواند بیش از عددی که نوشته‌اید مصرف کند. هنگامی که کانتینر سعی می‌کند از این حد فراتر رود، هسته یکی از پردازش‌های داخل آن را می‌کشد و کانتینر معمولاً با کد 137 خارج می‌شود.

این موضوع در VPS که مقدار RAM ثابت است و حافظه اضافی برای قرض گرفتن از میزبان وجود ندارد، اهمیت حیاتی دارد. یک کانتینر با نشت حافظه (memory leak) یا یک کوئری مخرب می‌تواند تمام صفحات آزاد حافظه را در یک سرور 8GB اشغال کند. در این حالت، هسته پردازشی را که تشخیص دهد «بدترین» است می‌کشد؛ که اغلب دیتابیس یا نشست SSH شماست، نه آن کانتینری که مشکل را ایجاد کرده است. محدودیت‌ها باعث می‌شوند که خرابی کل سرور به ری‌استارت شدن یک سرویس محدود شود.

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 امروزه می‌خواند.

هر دو روی یک میزبان واحد کار می‌کنند. Docker Compose، که همان پلاگین docker compose است، هنگام اجرای docker compose up، مقادیر deploy.resources.limits و deploy.resources.reservations را اعمال می‌کند، بدون اینکه نیازی به وجود کلاستر Swarm باشد. این پلاگین اکنون در شاخه v5 قرار دارد (نسخه v5.6.0 آخرین نسخه پایدار در زمان بررسی در تاریخ 2026-10-10 بود). این نسخه مستقیماً از v2 به v5 جهش کرد تا با نسخه‌های قدیمی فرمت فایل Compose اشتباه گرفته نشود، و هیچ‌یک از نسخه‌های v5 نحوه مدیریت محدودیت‌های منابع را تغییر نداده‌اند. بخش‌های مرتبط با Swarm در بلوک deploy، کلیدهای دیگری مانند placement، update_config و endpoint_mode هستند که زمان‌بندی و استقرار (rollout) در یک کلاستر از گره‌ها را برای docker stack deploy توصیف می‌کنند. بنابراین، این توصیه رایج که "deploy به Swarm نیاز دارد" برای زیربخش resources نادرست است و پیروی از آن باعث می‌شود سرویس‌های شما بدون هیچ محدودیتی اجرا شوند.

در هر پروژه، یکی از این دو نگارش را انتخاب کنید. نوشتن mem_limit: 512m و deploy.resources.limits.memory: 1g در یک سرویس واحد، فراتر از دشواری در خواندن است: مشخصات Compose بیان می‌کند که وقتی mem_limit تنظیم می‌شود، باید با limits.memory سازگار باشد، و مشخص نمی‌کند که در صورت عدم توافق، کدام مقدار اولویت دارد. از هر نگارشی که استفاده می‌کنید، تأیید کنید که daemon در واقع چه چیزی را اعمال کرده است:

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

مقادیر حافظه بر حسب بایت هستند، بنابراین 1g به صورت 1073741824 چاپ می‌شود. CPU بر حسب نانو-CPU است، بنابراین 1.5 به صورت 1500000000 چاپ می‌شود. مقدار 0 در هر فیلد به این معنی است که هیچ محدودیتی تعیین نشده است. کوچک‌ترین محدودیت حافظه‌ای که Docker می‌پذیرد 6m است و کمتر از آن، کانتینر از شروع به کار خودداری می‌کند.

وقتی یک کانتینر به محدودیت می‌رسد چه اتفاقی می‌افتد

کانتینر کند نمی‌شود، بلکه متوقف می‌شود.

هنگامی که یک پردازش درخواست صفحه (page) می‌کند و cgroup در حال حاضر در memory.max خود است، هسته ابتدا آنچه را که می‌تواند در آن cgroup بازیابی کند، بازپس می‌گیرد: کش صفحه تمیز (clean page cache) و سپس صفحاتی که قابل swap هستند. اگر این بازیابی فضای کافی آزاد نکند، OOM (خارج از حافظه) 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 را نادیده گرفته است. این تمایز ساعت‌ها در وقت صرفه‌جویی می‌کند، زیرا این دو مشکل هیچ وجه اشتراکی با هم ندارند.

دو مکان دیگر نیز این رویداد را ثبت می‌کنند. دیمون را به‌صورت زنده مشاهده کنید:

docker events --filter event=oom

سپس لاگ هسته را بخوانید که رکوردی است که پس از راه‌اندازی مجدد باقی می‌ماند:

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

یک kill در cgroup خطی را چاپ می‌کند که با Memory cgroup out of memory: Killed process 24713 (node) شروع می‌شود. آن خط به این معنی است که یک کانتینر به محدودیت خود رسیده است و می‌تواند در حالی ظاهر شود که میزبان هنوز مقدار زیادی RAM آزاد دارد. خطی که بدون پیشوند Memory cgroup باشد، یک OOM در سطح میزبان است، به این معنی که خود ماشین با کمبود RAM مواجه شده است. این همان شکستی است که محدودیت‌ها برای جلوگیری از آن در نظر گرفته شده‌اند، بنابراین دیدن آن معمولاً به این معنی است که مجموع محدودیت‌های شما بیش از حد بالا است، برخی سرویس‌ها هیچ محدودیتی ندارند، یا چیزی خارج از Docker از فضای خالی که باقی گذاشته‌اید، بیشتر استفاده می‌کند.

با restart: unless-stopped، یک حلقه OOM به خوبی پنهان می‌شود، زیرا سرویس یک ثانیه پس از مرگ، در docker compose ps بالا می‌آید. ستون uptime و تعداد restart را بررسی کنید و محدودیت را با یک healthcheck که برنامه را ناسالم گزارش می‌کند جفت کنید تا کانتینری که مدام می‌میرد، بدون نیاز به نظارت مداوم شما، قابل مشاهده باشد.

رزرو (Reservation) یک پیشنهاد است، اما محدودیت (Limit) یک قانون است

reservations.memory (که در نسخه‌های قدیمی‌تر mem_reservation نامیده می‌شد) یک کف نرم (soft floor) است. Docker آن را به عنوان یک محدودیت نرم توصیف می‌کند که وقتی daemon با رقابت بر سر منابع یا کمبود حافظه روی میزبان مواجه شود، فعال می‌گردد. این مقدار هرگز مانع از فراتر رفتن مصرف حافظه کانتینر از آن نمی‌شود و تضمین نمی‌کند که هنگام درخواست کانتینر، حافظه حتماً آزاد باشد. این پارامتر فقط باعث می‌شود هسته سیستم‌عامل (kernel) اولویت آزادسازی حافظه را به کانتینرهایی بدهد که از مقدار رزرو خود فراتر رفته‌اند.

بنابراین، رزرو به تنهایی از چیزی محافظت نمی‌کند. از آن برای مشخص کردن سرویسی استفاده کنید که می‌خواهید تحت فشار، با آن با ملایمت رفتار شود، و برای امنیت، به محدودیت (limit) تکیه کنید. مقدار رزرو را کمتر از محدودیت نگه دارید، در غیر این صورت کانتینر اجرا نخواهد شد: 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 نیست. این مقدار مجموع حافظه RAM و swap است. با تنظیم mem_limit: 1g و memswap_limit: 2g، کانتینر 1 گیگابایت RAM و 1 گیگابایت 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 واقع‌بین باشید. swap باعث می‌شود فرآیند OOM kill کندتر رخ دهد، نه اینکه احتمال وقوع آن کمتر شود؛ چرا که یک پردازش دارای نشت حافظه (leaking process)، همان‌طور که RAM را پر می‌کند، swap را نیز پر خواهد کرد. در همین حال، کانتینری که در حال استفاده شدید از swap روی فضای ذخیره‌سازی اشتراکی VPS است، سرعت تمام سرویس‌های دیگر روی آن سرور را کاهش می‌دهد. برای هر سرویسی که به تأخیر (latency) حساس است، اعمال یک محدودیت دقیق بدون swap باعث می‌شود سرویس در صورت بروز خطا، سریع‌تر و قابل‌پیش‌بینی‌تر از کار بیفتد.

چرا میزان مصرف حافظه بدتر از آنچه هست به نظر می‌رسد

در لینوکس، مقدار MEM USAGE در docker stats فقط حافظه کش غیرفعال (inactive_file در cgroup v2) را کسر می‌کند، بنابراین کشی که اخیراً استفاده شده همچنان محاسبه می‌شود و کانتینری که فایل‌های حجیم می‌خواند، به سمت محدودیت خود بالا می‌رود و در همان سطح باقی می‌ماند. این وضعیت عادی است و نشت حافظه محسوب نمی‌شود، زیرا کش‌های تمیز پیش از آنکه OOM killer فراخوانی شود، بازپس‌گیری می‌شوند. سرویسی مانند یک سرور رسانه Jellyfin که به صورت self-hosted اجرا می‌شود دقیقاً به همین دلیل، همواره نزدیک به سقف حافظه خود به نظر می‌رسد.

عدد را از داخل کانتینر به دو بخش کش و working set واقعی تقسیم کنید:

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) است، یعنی working set که قابل حذف نیست. file حافظه کش (page cache) است که قابل حذف می‌باشد. محدودیت خود را بر اساس anon به علاوه یک حاشیه اطمینان تنظیم کنید، نه بر اساس مجموع کل. فایل memory.events به این بحث پایان می‌دهد: اگر شمارنده oom_kill بزرگتر از صفر باشد، یعنی هسته سیستم‌عامل از زمان شروع کانتینر، چیزی را در آن کشته است و اگر شمارنده max در حال افزایش باشد، یعنی کانتینر در حال حاضر در سقف محدودیت خود نگه داشته شده است. هر دو دستور به shell و coreutils در داخل image نیاز دارند، بنابراین در imageهای distroless یا scratch با خطا مواجه می‌شوند.

محدودیت‌های ابعاد در یک VPS با 8GB رم

از میزبان (host) شروع کنید، نه از برنامه‌ها. در یک VPS با 8GB رم، حدود 1GB را برای کرنل، Docker daemon، sshd، journald و shell کاربری خود کنار بگذارید. این کار حدود 7GB فضا برای تخصیص باقی می‌گذارد و مجموع محدودیت‌های هر container باید زیر این مقدار بماند. بیش‌تخصیص (overcommitting) منابع تا روزی که دو سرویس همزمان به پیک مصرف برسند، کار می‌کند.

یک تقسیم‌بندی عملی برای یک سرور 8GB:

  • Reverse proxy: محدودیت 128m. این یک پردازش کوچک است و چنین محدودیت سخت‌گیرانه‌ای باعث می‌شود که در صورت بروز خطا در reload پیکربندی، بلافاصله متوجه شوید.
  • PostgreSQL: محدودیت 2g، با تنظیم shared_buffers روی حدود 512MB در فایل پیکربندی دیتابیس.
  • Application container: محدودیت 1g.
  • Background worker: محدودیت 512m.
  • Media or file service: محدودیت 2g، که بخش عمده آن صرف page cache خواهد شد.

این اعداد را عیناً در stack خود کپی نکنید. سرویس‌ها را یک روز تحت بار واقعی اجرا کنید، docker stats را زیر نظر بگیرید، مقدار پیک anon را برای هر container یادداشت کنید و حدود نصف آن را به عنوان فضای اطمینان (headroom) اضافه کنید. محدودیت بیش از حد سخت‌گیرانه از نداشتن محدودیت بدتر است، زیرا باعث می‌شود یک سرویس سالم در طول یک جهش ترافیکی عادی، توسط سیستم کشته شود.

یک تله وجود دارد که باید به آن توجه ویژه داشت. برخی runtimeها تا زمانی که به آن‌ها نگویید، محدودیت را نادیده می‌گیرند و آن‌هایی هم که آن را می‌خوانند، ممکن است خود را آن‌طور که انتظار دارید تنظیم نکنند. PostgreSQL با خوشحالی shared_buffers و work_mem را فراتر از محدودیت container خود تنظیم می‌کند و در نهایت کشته می‌شود. یک JVM (ماشین مجازی جاوا) مدرن به‌طور پیش‌فرض محدودیت cgroup را تشخیص می‌دهد، اما نسخه‌های HotSpot مانند OpenJDK تنها 25 درصد از آن را به heap اختصاص می‌دهند؛ بنابراین -XX:MaxRAMPercentage=75 روش معمول برای اجازه دادن به heap جهت استفاده از بخش بیشتری از محدودیت است، در حالی که فضایی برای حافظه خارج از heap باقی می‌ماند. Node.js از نسخه 12 به بعد، heap پیش‌فرض خود را بر اساس محدودیت cgroup و حدود نصف آن تنظیم می‌کند و در حدود 2GB به ثبات می‌رسد؛ بنابراین تنها زمانی --max-old-space-size را به مگابایت تنظیم کنید که سقف متفاوتی مد نظر دارید و آن را زیر محدودیت container نگه دارید تا garbage collector پیش از مداخله کرنل عمل کند. Ollama نیز همین وضعیت را با یک پارامتر متفاوت دارد، زیرا افزایش num_ctx باعث رشد KV cache به اندازه صدها مگابایت می‌شود و container در میانه یک prompt طولانی از کار می‌افتد. cgroup مذاکره نمی‌کند. فقط می‌کشد.

محدودیت‌های CPU رفتار کاملاً متفاوتی دارند

cpus: "1.5" به معنای 150 درصد از یک هسته است که به عنوان سهمیه CFS (زمان‌بندی کاملاً منصفانه) اعمال می‌شود. کانتینر در هر دوره 100 میلی‌ثانیه‌ای، 150 میلی‌ثانیه زمان CPU دریافت می‌کند که بین تمام ترد‌های آن تقسیم می‌شود. هنگامی که این سهمیه تمام شود، هسته سیستم‌عامل آن را تا شروع دوره بعدی منتظر نگه می‌دارد.

این تفاوت مهمی است. کانتینری که از محدودیت حافظه فراتر رود، کشته (kill) می‌شود. اما کانتینری که از محدودیت CPU فراتر رود، throttled شده و به کار خود با سرعت کمتر ادامه می‌دهد. بنابراین تنظیم محدودیت CPU با سخت‌گیری زیاد ایمن است، در حالی که محدودیت حافظه به فضای خالی (headroom) نیاز دارد.

cpu_shares ابزار متفاوتی است: یک وزن نسبی که تنها زمانی اهمیت پیدا می‌کند که CPUها واقعاً اشباع شده باشند. دو کانتینر با سهم‌های 1024 و 512، یک هسته مشغول را تقریباً با نسبت دو به یک تقسیم می‌کنند و در یک سیستم بیکار، هیچ‌کدام محدود نمی‌شوند. از سهم‌ها (shares) برای اولویت‌بندی سرویس‌ها استفاده کنید و از cpus زمانی استفاده کنید که به یک سقف واقعی نیاز دارید؛ برای مثال، برای جلوگیری از اینکه یک عملیات تبدیل فرمت (transcode) شبانه، منابع وب‌سرور شما را تخلیه کند.

FAQ

آیا deploy.resources.limits بدون Docker Swarm کار می‌کند؟

بله. Docker Compose، با استفاده از پلاگین docker compose (که اکنون در نسخه v5 است)، هنگام اجرای docker compose up روی یک میزبان تکی، deploy.resources.limits و deploy.resources.reservations را اعمال می‌کند. این موضوع را با docker inspect --format '{{.HostConfig.Memory}}' <container> تأیید کنید؛ این دستور محدودیت را به بایت نمایش می‌دهد و در صورت عدم اعمال محدودیت، 0 را چاپ می‌کند. کلیدهای داخل deploy که با در نظر گرفتن Swarm نوشته شده‌اند، مربوط به کلاستر هستند، مانند placement، update_config و endpoint_mode.

کد خروج 137 در Docker Compose به چه معناست؟

این کد تقریباً همیشه به این معنی است که پردازش اصلی سیگنال SIGKILL را دریافت کرده است، زیرا 137 حاصل جمع 128 و سیگنال 9 است (یک برنامه ممکن است عمداً با کد 137 خارج شود، اما این اتفاق نادر است). قاتل OOM (OOM killer) در هسته سیستم‌عامل دلیل رایج این اتفاق است، اما اگر برنامه سیگنال SIGTERM را نادیده بگیرد، وقفه زمانی خاموش‌سازی (shutdown timeout) نیز همین کد را تولید می‌کند. برای تشخیص این دو از هم، 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 Specification است و برای فایل‌های جدید گزینه بهتری محسوب می‌شود. اگر بقیه فایل شما از کلیدهای سطح بالای قدیمی استفاده می‌کند، mem_limit را حفظ کنید. Compose Specification ایجاب می‌کند که در صورت تنظیم هر دو، مقادیر آن‌ها یکسان باشد؛ بنابراین یکی را انتخاب کرده و نتیجه را با 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 نشان‌دهنده کانتینری است که در حال انجام عملیات ورودی/خروجی دیسک است، نه کانتینری که در آستانه توقف قرار دارد.

روی یک VPS با 8GB رم، چقدر حافظه باید تخصیص‌نیافته باقی بگذارم؟

حدود 1GB را برای هسته سیستم‌عامل، Docker daemon، sshd، journald و shell خود در نظر بگیرید و مجموع محدودیت‌های تمام کانتینرها را زیر 7GB باقی‌مانده نگه دارید. پیش از نهایی کردن اعداد، مقدار اوج anon هر کانتینر را تحت بار واقعی به مدت یک روز زیر نظر بگیرید و مجموع آن را به عنوان یک بودجه در نظر بگیرید، نه هدفی که باید حتماً پر شود.

آیا این مقاله مفید بود؟