SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-10-19

Как ограничить память контейнера в Docker Compose

Узнайте, как настроить deploy.resources и mem_limit в Docker Compose, чтобы избежать ошибки 137 и падения VPS. Практическое руководство по управлению RAM и CPU для сервисов.

Что делает ограничение памяти в 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, которую сегодня считывает docker compose.

Оба варианта работают на одном хосте. Docker Compose, плагин docker compose, применяет deploy.resources.limits и deploy.resources.reservations при выполнении docker compose up, даже если кластер Swarm отсутствует. Плагин сейчас находится в ветке v5 (версия v5.6.0 была последним стабильным релизом на 2026-10-10). Он перешел с v2 сразу на v5, пропустив 3 и 4, чтобы избежать путаницы со старыми версиями формата файлов Compose, и ни один релиз v5 не менял способ обработки лимитов ресурсов. Ключи блока deploy, ориентированные на Swarm, — это другие параметры, такие как placement, update_config и endpoint_mode, которые описывают планирование и развертывание по кластеру узлов для docker stack deploy. Таким образом, распространенный совет о том, что "deploy требует Swarm", неверен для подраздела resources, и следование ему оставляет ваши сервисы без каких-либо ограничений.

Выберите один вариант написания для проекта. Использование mem_limit: 512m и deploy.resources.limits.memory: 1g в одном сервисе не только затрудняет чтение: спецификация Compose гласит, что если задан mem_limit, он должен быть согласован с limits.memory, при этом не уточняется, какое значение будет приоритетным в случае конфликта. Какой бы вариант вы ни использовали, проверьте, что именно применил демон:

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

Значения памяти указываются в байтах, поэтому 1g отображается как 1073741824. Процессорное время измеряется в нано-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 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'

При завершении cgroup выводится строка, начинающаяся с Memory cgroup out of memory: Killed process 24713 (node). Эта строка означает, что один контейнер достиг своего собственного лимита; она может появиться, даже если на хосте много свободной оперативной памяти. Строка без префикса Memory cgroup означает OOM на уровне хоста, то есть на самой машине закончилась оперативная память. Это сбой, который должны предотвращать лимиты, поэтому его появление обычно означает, что сумма ваших лимитов слишком высока, некоторые сервисы не имеют лимитов вовсе или что-то вне Docker использует больше памяти, чем вы оставили в резерве.

При restart: unless-stopped OOM-цикл хорошо скрыт, так как сервис выглядит запущенным в docker compose ps через секунду после падения. Проверяйте столбец времени работы (uptime) и счетчик перезапусков, а также дополните лимит проверкой работоспособности (healthcheck), которая помечает приложение как неисправное, чтобы постоянно падающий контейнер был заметен без вашего участия.

Reservation — это рекомендация, limit — это правило

reservations.memory (более старый mem_reservation) является мягким порогом. Docker описывает его как мягкое ограничение, которое активируется, когда демон обнаруживает нехватку ресурсов или дефицит памяти на хосте. Он не запрещает контейнеру потреблять больше памяти и не гарантирует, что память будет свободна в момент запроса. Он лишь задает приоритет ядру: в первую очередь освобождать память тех контейнеров, которые превысили свой уровень reservation.

Таким образом, reservation сама по себе ничего не защищает. Используйте её, чтобы выделить сервис, который должен работать стабильно при высокой нагрузке, а для обеспечения безопасности полагайтесь на limit. Устанавливайте значение reservation ниже значения limit, иначе контейнер не запустится: Docker отклонит конфигурацию с ошибкой Minimum memory limit can not be less than memory reservation limit.

Учет swap, начистоту

Большинство образов VPS поставляются вообще без файла подкачки. Выполните swapon --show и free -h. Если общий объем swap равен нулю, все приведенные ниже настройки подкачки не имеют смысла, а лимит памяти является ограничением исключительно по RAM.

memswap_limit — это не объем 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. В то же время контейнер, активно использующий swap на общем дисковом хранилище VPS, замедляет работу всех остальных сервисов на сервере. Для задач, чувствительных к задержкам, корректный лимит без использования swap позволяет системе завершиться быстрее и более предсказуемо.

Почему использование памяти выглядит выше, чем есть на самом деле

В Linux значение MEM USAGE в docker stats вычитает только неактивный кэш страниц (inactive_file в cgroup v2), поэтому недавно использованный кэш продолжает учитываться. Контейнер, который считывает большие файлы, приближается к своему лимиту и остается на этом уровне. Это нормальное поведение, а не утечка, так как чистый кэш освобождается до того, как будет вызван 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 означает, что контейнер прямо сейчас удерживается на пределе своего лимита. Обе команды требуют наличия оболочки и coreutils внутри образа, поэтому они не сработают в образах distroless или scratch.

Ограничения ресурсов на VPS с 8GB ОЗУ

Начинайте планирование с хоста, а не с приложений. На VPS с 8GB ОЗУ оставьте около 1GB для ядра, Docker daemon, sshd, journald и вашей пользовательской оболочки. Это оставляет примерно 7GB для распределения; сумма лимитов всех контейнеров не должна превышать это значение. Оверкоммит ресурсов работает до тех пор, пока два сервиса не достигнут пиковой нагрузки одновременно.

Рабочий вариант распределения ресурсов на сервере с 8GB:

  • Reverse proxy: лимит 128m. Это небольшой процесс, и такой жесткий лимит позволяет сразу обнаружить ошибки при перезагрузке конфигурации.
  • PostgreSQL: лимит 2g, при этом shared_buffers в конфигурации базы данных может быть установлен на уровне около 512MB.
  • Контейнер приложения: лимит 1g.
  • Фоновый обработчик: лимит 512m.
  • Сервис медиафайлов: лимит 2g, большая часть которого будет занята кэшем страниц.

Не копируйте эти значения в свой стек. Запустите сервисы под реальной нагрузкой на сутки, отслеживайте docker stats, возьмите пиковое значение anon для каждого контейнера и добавьте примерно половину этого объема в качестве запаса. Слишком жесткий лимит хуже, чем его отсутствие, так как он завершает работу исправного сервиса во время обычного скачка трафика.

Одна ловушка заслуживает отдельного упоминания. Некоторые среды выполнения игнорируют лимит, если явно не указать им на него, а те, что учитывают, могут распределять память не так, как вы ожидаете. PostgreSQL может без проблем выделить shared_buffers и work_mem сверх лимита контейнера, после чего будет принудительно завершен. Современная JVM (Java virtual machine) по умолчанию определяет лимит cgroup, но сборки HotSpot, такие как OpenJDK, выделяют куче (heap) только 25 процентов от него, поэтому -XX:MaxRAMPercentage=75 — это стандартный способ позволить куче использовать большую часть лимита, оставляя место для памяти вне кучи. Node.js определяет размер кучи по умолчанию на основе лимита cgroup начиная с версии 12, устанавливая его на уровне около половины лимита контейнера с потолком около 2GB. Устанавливайте --max-old-space-size в мегабайтах только в том случае, если вам нужен другой предел, и держите его ниже лимита контейнера, чтобы сборщик мусора успел сработать до вмешательства ядра. С Ollama ситуация аналогична, но используется другой параметр, так как увеличение num_ctx расширяет KV-кэш на сотни мегабайт, из-за чего контейнер завершается в процессе обработки длинного запроса. Cgroup не ведет переговоры. Она завершает процесс.

Ограничения CPU работают иначе

cpus: "1.5" означает 150% одного ядра, что реализуется через квоту CFS (completely fair scheduler). Контейнер получает 150 мс процессорного времени на каждые 100 мс периода, распределяя его между всеми своими потоками. Когда лимит исчерпан, ядро заставляет процесс ждать начала следующего периода.

В этом заключается важное различие. Если контейнер превышает лимит памяти, он завершается принудительно. Если контейнер превышает лимит CPU, он подвергается троттлингу и продолжает работу, но медленнее. Поэтому лимиты CPU можно устанавливать агрессивно, тогда как для памяти всегда требуется запас.

cpu_shares — это другой инструмент: относительный вес, который учитывается только при реальной загрузке процессора. Два контейнера с долями 1024 и 512 делят нагруженное ядро примерно в пропорции два к одному, а на простаивающем сервере ни один из них не ограничивается. Используйте доли для приоритизации сервисов, а cpus применяйте, когда нужно установить жесткий потолок, например, чтобы ночная задача перекодирования не лишала ресурсов ваш веб-сервер.

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 ядра, но таймаут завершения работы даёт тот же код, если приложение игнорирует 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, который ядро очищает при нехватке памяти, вместо того чтобы вызывать OOM kill. Запустите docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat и посмотрите значение anon — это рабочий набор памяти, который нельзя освободить. Высокое значение file при низком anon означает, что контейнер выполняет операции ввода-вывода, а не готовится к завершению.

Сколько оперативной памяти оставить свободной на VPS с 8GB RAM?

Оставьте около 1GB для ядра, Docker daemon, sshd, journald и вашей оболочки, а сумму лимитов всех контейнеров держите в пределах оставшихся 7GB. В течение дня отслеживайте пиковое значение anon для каждого контейнера под реальной нагрузкой, прежде чем фиксировать лимиты, и рассматривайте общую сумму как бюджет, а не как цель, которую нужно заполнить.

Была ли эта статья полезной?