SSD Nodes Learn Hosting plans →
راهنماها Matt Connorتوسط Matt Connor

محدود کردن رم و سی‌پی‌یو در Docker Compose

با تنظیم deploy.resources در فایل docker-compose.yml از کرش کردن سرور جلوگیری کنید. این راهنما نحوه مدیریت mem_limit برای رفع خطای 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-06 بود). این نسخه مستقیماً از v2 به v5 جهش کرد تا با نسخه‌های قدیمی فرمت فایل Compose اشتباه گرفته نشود؛ هیچ‌کدام از نسخه‌های v5 نحوه مدیریت محدودیت‌های منابع را تغییر نداده‌اند. بخش‌های مختص Swarm در بلوک deploy شامل کلیدهای دیگری است: 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 است و کمتر از آن، کانتینر از اجرا امتناع می‌کند.

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

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

هنگامی که یک پردازش درخواست یک صفحه (page) می‌کند و cgroup در حال حاضر در memory.max خود قرار دارد، هسته (kernel) ابتدا آنچه را که می‌تواند در آن 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 را ارسال کرده است و دلیل معمول آن، رسیدن docker compose stop به دوره مهلت ده ثانیه‌ای خود است، زیرا برنامه SIGTERM را نادیده گرفته است. تشخیص این تفاوت ساعت‌ها در وقت شما صرفه‌جویی می‌کند، زیرا این دو مشکل هیچ وجه اشتراکی با هم ندارند.

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

docker events --filter event=oom

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

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 و تعداد 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 شامل page cache نیز می‌شود؛ بنابراین کانتینری که فایل‌های حجیم می‌خواند، به سمت محدودیت خود پیش می‌رود و در همان سطح باقی می‌ماند. این رفتار عادی است و نشت حافظه (leak) محسوب نمی‌شود، زیرا cache تمیز پیش از آنکه OOM killer فعال شود، بازپس‌گیری می‌شود. سرویسی مانند یک سرور رسانه Jellyfin که خودتان میزبانی می‌کنید دقیقاً به همین دلیل، همواره نزدیک به سقف حافظه خود دیده می‌شود.

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

محدودیت‌های اندازه‌گیری در یک VPS با 8GB رم

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

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

  • Reverse proxy: محدودیت 128m. این یک پردازش کوچک است و چنین محدودیت سخت‌گیرانه‌ای، بارگذاری مجدد پیکربندی‌های معیوب را بلافاصله شناسایی می‌کند.
  • PostgreSQL: محدودیت 2g، با تنظیم shared_buffers روی حدود 512MB در فایل پیکربندی دیتابیس.
  • کانتینر برنامه: محدودیت 1g.
  • پردازش پس‌زمینه (Background worker): محدودیت 512m.
  • سرویس فایل یا رسانه: محدودیت 2g، که بیشتر آن صرف page cache خواهد شد.

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

یک تله وجود دارد که باید به آن توجه ویژه داشت. این محدودیت برای اکثر runtimeها نامرئی است مگر اینکه آن را به آن‌ها اطلاع دهید. PostgreSQL با خوشحالی shared_buffers و work_mem را فراتر از محدودیت کانتینر خود تنظیم می‌کند و در نهایت توسط سیستم کشته می‌شود. یک JVM (ماشین مجازی جاوا) به -XX:MaxRAMPercentage=75 نیاز دارد تا heap خود را بر اساس محدودیت cgroup تنظیم کند، نه بر اساس رم میزبان. Node.js به --max-old-space-size بر حسب مگابایت نیاز دارد که باید کمتر از محدودیت کانتینر تنظیم شود، در غیر این صورت garbage collector اجازه می‌دهد heap تا زمانی که کرنل مداخله کند، رشد کند. Ollama نیز همین وضعیت را با یک تنظیم متفاوت دارد، زیرا افزایش num_ctx باعث رشد KV cache به میزان صدها مگابایت می‌شود و کانتینر در میانه یک 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 نیاز دارند، mode، placement، update_config و endpoint_mode هستند.

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

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

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

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