SSD Nodes Learn 8GB RAM — $66/рік
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-01

Docker Compose: обмеження пам’яті без OOM

Налаштуйте memory та CPU limits у Docker Compose, щоб контейнер не поклав VPS: розберіться з deploy.resources, mem_limit, exit 137, swap і розміром лімітів.

Що робить обмеження пам’яті Docker Compose

Обмеження пам’яті Docker Compose — це жорстка межа, яку ядро Linux встановлює для cgroup (control group — функція ядра, що вимірює ресурси для набору процесів) одного контейнера. Установіть deploy.resources.limits.memory для сервісу, і цей контейнер не зможе використати більше пам’яті, ніж указано. Якщо контейнер перевищить межу, ядро завершить процес усередині контейнера, а контейнер зазвичай завершить роботу з кодом 137.

Це особливо важливо на VPS, де обсяг RAM фіксований і немає додаткової пам’яті хоста. Один контейнер із витоком пам’яті або невдалим запитом може використати всі вільні сторінки пам’яті на сервері з 8GB RAM. Потім ядро завершить процес, який вважатиме найменш важливим. Часто це буде база даних або ваш сеанс 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 Specification — формату, який 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 CPU, тому 1.5 виводить 1500000000. Значення 0 у будь-якому полі означає, що обмеження не встановлено. Мінімальне обмеження пам’яті, яке приймає Docker, — 6m. Якщо вказати менше, контейнер не запуститься.

Що відбувається, коли контейнер досягає ліміту

Контейнер не сповільнюється. Він аварійно завершує роботу.

Коли процес запитує сторінку, а cgroup уже досягла свого memory.max, ядро спочатку звільняє все можливе всередині цієї 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. Це розрізнення заощаджує години діагностики, оскільки ці дві проблеми не пов’язані між собою.

Подію також записують у двох інших місцях. Спочатку перегляньте журнал демона в реальному часі:

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 на хості. Це означає, що на машині закінчилася оперативна пам’ять. Саме цьому мають запобігати ліміти. Якщо ви бачите такий рядок, це ознака того, що сума лімітів зависока або деякі служби взагалі не мають ліміту.

З restart: unless-stopped цикл OOM важко помітити, оскільки служба знову з’являється у docker compose ps через секунду після завершення. Перевірте стовпець часу роботи та лічильник перезапусків. Також поєднайте ліміт із перевіркою стану, яка повідомляє, що застосунок є несправним, щоб контейнер, який постійно завершує роботу, було видно без постійного спостереження за ним.

Резервування — це підказка, а ліміт — правило

reservations.memory (старіший mem_reservation) — це м’який нижній поріг. Docker описує його як м’який ліміт, який активується, коли daemon виявляє конкуренцію за ресурси або низький обсяг пам’яті на host. Він не забороняє container перевищувати цей поріг і не гарантує наявність вільної пам’яті, коли вона потрібна container. Він лише схиляє kernel спочатку звільняти пам’ять із container, які перевищують своє резервування.

Отже, саме по собі резервування нічого не захищає. Використовуйте його, щоб позначити service, до якого потрібно ставитися пріоритетно за високого навантаження, а для безпеки покладайтеся на ліміт. Резервування має бути меншим за ліміт, інакше container не запуститься: 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, але не зменшує ймовірність такого завершення, оскільки процес із витоком пам’яті так само швидко заповнює swap, як і RAM. Водночас контейнер, який активно використовує swap на спільному сховищі VPS, уповільнює всі інші служби на цьому хості. Для служб, чутливих до затримок, коректне обмеження без swap забезпечує швидше та передбачуваніше завершення роботи.

Чому використання пам’яті виглядає гіршим, ніж є насправді

Показник MEM USAGE у docker stats містить кеш сторінок, тому контейнер, який читає великі файли, поступово наближається до свого ліміту й залишається на цьому рівні. Це нормально і не є витоком пам’яті, оскільки чистий кеш вивільняється ще до виклику 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 — це кеш сторінок, який можна вивільнити. Визначайте ліміт на основі anon із запасом, а не на основі загального значення. Файл memory.events остаточно підтверджує ситуацію: значення лічильника oom_kill понад нуль означає, що ядро завершило роботу певного процесу в цьому контейнері після його запуску, а зростання лічильника max означає, що контейнер саме зараз утримується на граничному значенні. Для обох команд потрібні shell і coreutils усередині образу, тому вони не працюють в образі distroless або scratch.

Ліміти розподілу ресурсів на VPS із 8GB

Починайте з хоста, а не із застосунків. На VPS із 8GB залиште приблизно 1GB для ядра, Docker daemon, sshd, journald і власної оболонки входу. Залишиться приблизно 7GB для розподілу, і сума лімітів усіх контейнерів не повинна перевищувати це значення. Надмірне виділення ресурсів працює доти, доки два сервіси не досягнуть пікового навантаження одночасно.

Практичний розподіл для сервера з 8GB:

  • Reverse proxy: ліміт 128m. Це невеликий процес, і такий жорсткий ліміт одразу виявляє неконтрольований перезапуск конфігурації.
  • PostgreSQL: ліміт 2g, а shared_buffers у конфігурації бази даних слід установити приблизно на 512MB.
  • Application container: ліміт 1g.
  • Background worker: ліміт 512m.
  • Media або file service: ліміт 2g, більшу частину якого буде використано під кеш сторінок.

Не копіюйте ці значення у власний стек. Протягом дня запускайте сервіси під реальним навантаженням, відстежуйте docker stats, визначте пікове значення anon для кожного контейнера та додайте приблизно половину цього значення як запас. Надто жорсткий ліміт гірший за відсутність ліміту, оскільки він зупиняє справний сервіс під час звичайного стрибка мережевого трафіку.

На одну проблему слід звернути окрему увагу. Більшість середовищ виконання не враховують цей ліміт, доки ви явно не повідомите їм про нього. PostgreSQL без проблем встановить значення shared_buffers і work_mem вище за ліміт контейнера, після чого процес буде примусово завершено. JVM (Java virtual machine) потребує -XX:MaxRAMPercentage=75, щоб визначати розмір heap на основі ліміту cgroup, а не обсягу RAM хоста. Node.js потребує --max-old-space-size у мегабайтах. Це значення слід установити нижче за ліміт контейнера, інакше garbage collector збільшуватиме heap, доки ядро не втрутиться. cgroup не узгоджує параметри. Вона завершує процес.

Обмеження CPU працюють зовсім інакше

cpus: "1.5" означає 150% одного ядра. Обмеження застосовується як квота CFS (completely fair scheduler). Контейнер отримує 150ms процесорного часу в кожному періоді тривалістю 100ms. Квота спільна для всіх його потоків. Після її вичерпання ядро змушує контейнер чекати на початок наступного періоду.

Це ключова відмінність. Контейнер, який перевищує обмеження пам’яті, завершується. Контейнер, який перевищує обмеження CPU, дроселюється та продовжує працювати повільніше. Тому обмеження CPU можна встановлювати із запасом у бік вищих значень, а для обмеження пам’яті потрібен запас.

cpu_shares — це інший інструмент: відносна вага, яка має значення лише тоді, коли CPU фактично повністю завантажені. Два контейнери зі значеннями shares 1024 і 512 ділять завантажене ядро приблизно у співвідношенні два до одного. На незавантаженій системі жоден із них не обмежується. Використовуйте shares, щоб визначати пріоритет служб, а cpus — коли потрібна фактична верхня межа, наприклад щоб нічне перекодування не позбавило ваш вебсервер ресурсів.

FAQ

Чи працює deploy.resources.limits без Docker Swarm?

Так. Compose V2 застосовує 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 у ядрі, але тайм-аут завершення роботи дає такий самий код, якщо застосунок ігнорує 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 Specification і кращий варіант за замовчуванням для нового файлу. Залиште mem_limit, якщо решта файлу вже використовує старі ключі верхнього рівня. Встановлення обох параметрів для одного сервісу лише ускладнює читання файлу, тому виберіть один варіант і перевірте результат за допомогою docker inspect.

Чому мій контейнер використовує весь ліміт пам’яті, але не завершується?

Показник використання у docker stats містить кеш сторінок. Ядро звільняє його під час нестачі ресурсів, не запускаючи завершення через OOM. Запустіть docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat і перегляньте значення anon. Це робочий набір пам’яті, який неможливо звільнити. Високе значення file разом із низьким значенням anon означає, що контейнер виконує введення-виведення на диску, а не готується до завершення.

Скільки RAM слід залишити невиділеною на VPS із 8GB?

Залиште приблизно 1GB для ядра, демона Docker, sshd, journald і власної оболонки. Потім обмежте суму лімітів усіх контейнерів значенням, меншим за решту 7GB. Протягом доби за реального навантаження відстежуйте пікове значення anon для кожного контейнера, перш ніж остаточно визначити значення лімітів. Розглядайте загальний обсяг як бюджет, а не як ціль, яку потрібно повністю використати.