ZIM-НИЙ SAAALEЗимние скидки: до −50% на старт и −20% на продление
до 31.01.2026 Подробнее
Выберите продукт

Docker Compose: лимиты CPU, памяти, PID и ulimits, диагностика OOM через dmesg на cgroup v2

Контейнер перезапускается, а в логах только «Killed»? Разберём, как задавать лимиты CPU, памяти и PID в Docker Compose, настраивать ulimits (nofile) и отличать OOM внутри cgroup от системного OOM по dmesg, journalctl и memory.events на Linux с cgroup v2.
Docker Compose: лимиты CPU, памяти, PID и ulimits, диагностика OOM через dmesg на cgroup v2

Когда контейнер «внезапно» перезапускается, приложение падает без стека, а в логах только «Killed», чаще всего виноват OOM (out-of-memory). И почти всегда рядом живут две проблемы: лимиты ресурсов настроены не так, как вы думаете, и диагностика ведётся не там, где нужно. Ниже — практический разбор, как в Docker Compose задавать ограничения по CPU/памяти/PID, настраивать ulimits (например, nofile), и как подтвердить OOM через dmesg на Linux, особенно на cgroup v2.

Ограничения Docker Compose: что реально работает и где

В Compose важно различать два режима, потому что синтаксис похож, а поведение — нет:

  • Обычный Docker Engine (команда docker compose на одном хосте): лимиты задаются напрямую в параметрах сервиса и превращаются в настройки контейнера.
  • Swarm (команда docker stack deploy): лимиты живут в deploy.resources.

Типичная ошибка: написать лимиты в deploy и ожидать их работы на одиночном Docker Engine. В этом сценарии секция deploy обычно игнорируется.

Проверяйте факт применения лимитов

Не доверяйте YAML «на глаз». После запуска подтверждайте, что лимиты реально применились:

docker compose up -d

docker compose ps

docker inspect -f '{{.HostConfig.NanoCpus}} {{.HostConfig.Memory}} {{.HostConfig.PidsLimit}}' $(docker compose ps -q app)

Для быстрой «картинки» по факту потребления ресурсов удобен docker stats:

docker stats --no-stream $(docker compose ps -q app)

Если используете systemd на хосте и хотите навести порядок в иерархии cgroup, пригодится материал: systemd slices и cgroup: как разнести сервисы по группам.

Лимиты CPU: cpus, квоты и типичные грабли

Параметр cpus — самый понятный способ «выдать» контейнеру долю процессора. Он задаёт квоту CPU (по времени), а не жёсткую привязку к конкретным ядрам.

Пример: ограничить сервис до 1.5 CPU

services:
  app:
    image: your-image
    cpus: 1.5

Практический смысл: в среднем контейнер сможет потреблять эквивалент 1.5 ядра суммарно, но при пиках будет «резаться» планировщиком.

Почему «вдруг тормозит» при низком cpus

  • Срезаются пики: приложения с короткими burst-нагрузками начинают копить очередь (особенно заметно на PHP/Node воркерах и фоновых джобах).
  • Неверные ожидания: cpus: 1 не означает «одно ядро навсегда», это именно квота по времени.
  • Шум соседей: при конкуренции за CPU даже без лимитов возможны задержки, а с квотой «потолок» становится жёстче.

Если нужно именно привязать к ядрам, используют CPU sets (в Compose встречается как cpuset или cpuset_cpus в зависимости от версии/реализации). Но для большинства веб-сервисов квота через cpus — практичнее и проще сопровождать.

Схема лимитов Docker Compose и проверка ограничений через docker stats

Если хотите, чтобы лимиты и поведение контейнеров были предсказуемыми (особенно по памяти и swap), удобнее держать окружение на выделенных ресурсах. Для таких задач обычно берут VDS, где вы контролируете конфигурацию хоста и политику памяти.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Лимиты памяти: mem_limit, swap и ловушки OOM

С памятью чаще всего ломаются ожидания: «я поставил лимит, значит OOM не будет». OOM будет, просто он станет предсказуемым и локальным (в пределах контейнера), если лимит выбран правильно.

Для одиночного Docker Engine в Compose обычно используют:

  • mem_limit — лимит RAM для контейнера;
  • memswap_limit — лимит RAM+swap (если swap доступен контейнерам и не отключён политиками хоста);
  • в Swarm — deploy.resources.limits.memory.

Пример: жёсткий лимит памяти 512M

services:
  app:
    image: your-image
    mem_limit: 512m

При превышении лимита контейнер может быть убит OOM-механизмом на уровне cgroup. Важно различать два сценария:

  • OOM внутри cgroup контейнера: контейнер превысил свой лимит (mem_limit).
  • Системный OOM на хосте: закончилась память на сервере, и ядро выбирает жертву среди процессов всего хоста (включая контейнеры).

Что меняется на cgroup v2

На cgroup v2 диагностика становится более «cgroup-ориентированной»: появляется файл memory.events со счётчиками событий (включая OOM) и более прозрачная картина того, что именно произошло.

Если видите «Killed», не ограничивайтесь логами приложения. На cgroup v2 часто быстрее подтвердить причину через сообщения ядра и счётчики cgroup, чем пытаться угадать по поведению сервиса.

Ограничение числа процессов: pids_limit как страховка от размножения

pids_limit — недооценённый, но очень полезный лимит. Он ограничивает количество процессов/тредов (в терминах ядра), которые может создать контейнер. Это помогает локализовать утечки процессов и защищает хост от перегрузки таблицы PID.

Пример: ограничить контейнер до 200 PID

services:
  worker:
    image: your-worker
    pids_limit: 200

Когда лимит выбран уместно, вы получаете контролируемую деградацию: вместо «упал весь сервер» сервис начнёт получать ошибки создания тредов/процессов, и вы быстрее поймёте, где проблема.

Практическая ремарка: Java, Node.js (workers), PHP-FPM, PostgreSQL и некоторые агенты мониторинга создают заметное число тредов. Подбирайте pids_limit по факту: сначала измерьте, потом ограничивайте.

ulimits: nofile и ошибки «too many open files»

Запрос «ulimits nofile» обычно появляется после ошибок вида «too many open files». В контейнерном мире это норма для нагруженных API, прокси, очередей и сервисов с большим числом одновременных соединений.

Пример: поднять лимит nofile

services:
  api:
    image: your-api
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
  • soft — рабочий лимит по умолчанию, hard — верхняя граница, до которой процесс может подняться сам.
  • Проверьте, что лимит наследует именно тот процесс, который обслуживает нагрузку (особенно если есть init/supervisor).
  • Если у хоста низкие системные лимиты, контейнер упрётся в них раньше. Тогда нужно смотреть ограничения systemd и настройки демона Docker.

Как проверить текущие ulimits внутри контейнера

docker exec -it $(docker compose ps -q api) sh -lc 'ulimit -n; ulimit -a'

Если образ distroless и shell отсутствует, проверка через docker exec может быть невозможна. В таких случаях ориентируйтесь на метрики приложения и поведение под нагрузкой, либо используйте отдельный debug-контейнер в той же сети/неймспейсах (в рамках вашей политики безопасности).

Диагностика: как подтвердить, что контейнер умер от OOM

У вас есть три источника правды: Docker (флаги состояния), ядро (логи) и счётчики cgroup.

1) Docker: флаг OOMKilled и код завершения

cid=$(docker compose ps -q app)

docker inspect -f 'State={{.State.Status}} ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}} Error={{.State.Error}}' $cid

OOMKilled=true — сильный индикатор OOM внутри лимита контейнера, но при системном OOM на хосте картина может быть менее очевидной. Поэтому всегда смотрим логи ядра.

2) Ядро: dmesg и journalctl -k

Классика поиска: «Out of memory», «oom-kill», «Killed process», а для контейнеров часто встречается «Memory cgroup out of memory».

dmesg -T | grep -E 'Out of memory|Killed process|oom-kill|Memory cgroup out of memory'

journalctl -k -b --no-pager | grep -E 'Out of memory|Killed process|oom-kill|Memory cgroup out of memory'
  • Строка про Memory cgroup out of memory обычно означает OOM в пределах лимита cgroup контейнера.
  • Строка Killed process даёт PID и имя процесса, часто рядом видно путь cgroup.

3) cgroup v2: memory.events, memory.current, memory.max

Если знаете путь cgroup контейнера в /sys/fs/cgroup, то счётчики OOM можно подтвердить напрямую. В примере ниже угловые скобки показаны как текст (подставьте реальный путь):

cat /sys/fs/cgroup/<path>/memory.current
cat /sys/fs/cgroup/<path>/memory.max
cat /sys/fs/cgroup/<path>/memory.events

В memory.events ищите счётчики oom и oom_kill. Это самый прямой ответ на вопрос «было ли OOM-событие в этой cgroup».

Как отличить: «упёрлись в лимит контейнера» или «на хосте кончилась память»

Это ключевой практический вопрос: лечится по-разному.

Признаки OOM из-за лимита контейнера

  • В docker inspect видно OOMKilled=true.
  • В логах ядра есть «Memory cgroup out of memory».
  • Остальные сервисы на хосте продолжают работать, нет общей «волны» падений.

Признаки системного OOM на хосте

  • OOM случается «волнами»: падают разные контейнеры и/или системные службы.
  • В логах ядра видно, что выбиралась жертва среди процессов хоста, а не только одного cgroup.
  • Перед падением наблюдается резкий рост потребления памяти и/или активный swap (если он включён).

Если контейнеры стабильно упираются в память на одном узле, проще масштабировать или вынести тяжёлые сервисы на отдельные ресурсы. Для этого часто выбирают виртуальный хостинг под лёгкие сайты и отдельный VDS под контейнеры с гарантированными лимитами и контролем swap.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Практическая стратегия настройки лимитов в Compose

Лимиты — это не про «закрутить гайки», а про предсказуемость. Лучше ограниченно деградировать один сервис, чем устроить системный OOM и уронить весь хост.

Рекомендованный порядок

  1. Снимите базовую статистику: пики RAM/CPU, число соединений, открытых файлов, процессов/тредов.
  2. Поставьте мягкие, но реальные лимиты: память с запасом 20–40% от наблюдаемого пика, CPU по вашему SLA, pids_limit и nofile по фактической нагрузке.
  3. Подтвердите применённые значения через docker inspect, docker stats и ulimit внутри контейнера.
  4. Проведите тест отказа: нагрузочный тест и контролируемое давление по памяти. Зафиксируйте, как выглядит инцидент в логах и какие команды вы запускаете.

Базовый compose-фрагмент (CPU + RAM + PIDs + ulimits)

services:
  app:
    image: your-image
    cpus: 1.0
    mem_limit: 768m
    pids_limit: 300
    ulimits:
      nofile:
        soft: 65536
        hard: 65536

Это не «идеальные числа», а каркас. Веб-API чаще упирается в nofile, воркеры очередей — в CPU, а сервисы с утечками — в память и PID.

Падения по OOM часто маскируются политиками перезапуска и healthcheck: контейнер успевает умереть и подняться, а корень причины теряется. Если у вас сложные зависимости, проверьте настройки осознанно: Docker healthcheck и restart policy: как не устроить бесконечный рестарт.

Логи ядра Linux с OOM kill и указанием cgroup для контейнера

Частые ошибки и быстрые проверки

Ошибка 1: лимиты написаны в deploy, но вы не в Swarm

Симптом: контейнер ест всю память/CPU, хотя «в compose всё ограничили». Лечение: переносите лимиты в параметры сервиса (например, cpus, mem_limit, pids_limit) или действительно включайте Swarm-режим и деплойте стеком.

Ошибка 2: OOM «прячется», потому что смотрят только логи приложения

Симптом: приложение просто «умирает» без ошибок. Лечение: проверяйте docker inspect и сообщения ядра в dmesg/journalctl -k.

Ошибка 3: подняли mem_limit, а упёрлись в дескрипторы

Симптом: сетевые ошибки под нагрузкой, «too many open files», странные отказы подключений. Лечение: подберите ulimits.nofile и убедитесь, что лимит реально виден основному процессу.

Мини-набор команд «на инцидент»

docker compose ps

docker inspect -f 'Exit={{.State.ExitCode}} OOM={{.State.OOMKilled}} Finished={{.State.FinishedAt}}' $(docker compose ps -q app)

dmesg -T | tail -n 200

journalctl -k -b --no-pager | tail -n 200

docker stats --no-stream

Итог

Лимиты в Docker Compose — это про управляемость. Минимальный практический набор: cpus для CPU, mem_limit для памяти, pids_limit как защита от «размножения» и ulimits (особенно nofile) для сетевых/веб-нагрузок. А если что-то упало — подтверждайте OOM через docker inspect и сообщения ядра в dmesg/journalctl -k; на cgroup v2 дополнительно опирайтесь на memory.events, чтобы быстро понять, было ли OOM внутри cgroup.

Поделиться статьей

Вам будет интересно

IPv6 на сервере: SLAAC vs static, privacy extensions и firewall без сюрпризов OpenAI Статья написана AI (GPT 5)

IPv6 на сервере: SLAAC vs static, privacy extensions и firewall без сюрпризов

Разбираем, как сервер получает IPv6 через SLAAC и почему для продакшена чаще нужен статический адрес. Объясняем privacy extensions ...
MX migration без простоя: dual delivery, TTL, приоритеты и план отката OpenAI Статья написана AI (GPT 5)

MX migration без простоя: dual delivery, TTL, приоритеты и план отката

Перенос почты — это не просто смена MX. В статье — практичный план MX migration без простоя: как заранее снизить DNS TTL, выставит ...
systemd-journald и syslog: хранение, ротация и форвардинг логов в Linux OpenAI Статья написана AI (GPT 5)

systemd-journald и syslog: хранение, ротация и форвардинг логов в Linux

Разбираем, как в Linux устроены логи с systemd-journald и syslog: где хранится journal, как включить Storage=persistent, ограничит ...