Короткий ответ

Без лимитов контейнер web съест RAM/CPU/pids хоста host.example. В Compose v2 задайте mem_limit/memory, cpus, pids_limit (и при необходимости memswap_limit). Проверьте cgroup в docker inspect и docker stats. Слишком жёсткий memory даст OOMKilled и restart loop — тогда поднимите лимит по факту RSS, не снимайте его. Не privileged: privileged ослабляет изоляцию cgroup.

Engine 27.x на Ubuntu 22.04/24.04 — cgroup v2, драйвер systemd.

Симптомы и как отличить

  • dmesg OOM killer на python из контейнера, хост подвис;
  • docker stats MEM 12 GiB при 16 GiB хоста, лимит 0;
  • сотни процессов java — нет pids;
  • db тормозит, когда web компилирует.
Поле inspect0 / пустоЗадано
Memoryбезлимитбайты
NanoCpus / CpuQuotaбезлимитдоля CPU
PidsLimitчасто 0N

Возможные причины

  1. YAML без memory/cpus.
  2. Лимиты только в deploy:, локальный Compose их игнорирует.
  3. docker update забыли после recreate.
  4. Privileged / свой cgroup-parent сломал учёт.
  5. Редко: nproc в образе vs pids cgroup.

Диагностика

docker inspect app-web-1 --format 'mem={{.HostConfig.Memory}} swap={{.HostConfig.MemorySwap}} ncpu={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}} cpu_shares={{.HostConfig.CpuShares}}'
docker stats --no-stream app-web-1 app-db-1
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect -f '{{.Id}}' app-web-1).scope/memory.max 2>/dev/null || true

На cgroup v2 путь может быть под docker.slice. Если не нашли — достаточно inspect HostConfig.

cd /opt/app
docker compose config | grep -n -A15 -E 'mem_|memory:|cpus:|pids'

Текущий RSS vs лимит: если RSS 1.8 GiB при лимите 2 GiB — запас мал.

Решение

Сценарий A. Compose на одном хосте (практика 27.x)

Проверьте, что ваш файл после docker compose up ставит HostConfig. Надёжные ключи в распространённом Compose:

services:
  web:
    mem_limit: 512m
    mem_reservation: 256m
    cpus: 1.0
    pids_limit: 256

Либо эквивалент в спецификации:

    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
          pids: 256
        reservations:
          memory: 256M

Обязательно после изменения:

docker compose up -d --force-recreate web
docker inspect app-web-1 --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}} {{.HostConfig.PidsLimit}}'

Если Memory всё ещё 0 — ваш плагин не прокинул deploy; используйте mem_limit/cpus как в первом блоке (они живут в compose с давних версий для non-swarm).

Сценарий B. Живой контейнер, окно минимально

docker update --memory 512m --memory-swap 640m --cpus 1.0 --pids-limit 256 app-web-1
docker inspect app-web-1 --format '{{.HostConfig.Memory}} {{.HostConfig.PidsLimit}}'

Внесите те же значения в YAML, иначе следующий up сотрёт.

Swap: если memory-swap = memory, swap контейнеру фактически нет. На cgroup v2 поведение swap зависит от хоста; не отключайте хостовый swap сюрпризом без политики.

Сценарий C. OOM после введения лимита

Не снимайте лимит. Снимите docker stats, профилируйте приложение, поднимите memory осознанно (например 512→768). Проверьте утечки. Код 137 + OOMKilled true — см. restart-статью.

Сценарий D. CPU

cpus: 1.0 = один CPU. Для сборки в том же сервисе вынесите build на CI, не раздувайте прод-лимит «на всякий compile».

Сценарий E. pids

pids_limit: 256 (подберите). Fork-бомба и «too many open processes» ловятся здесь. Слишком низко сломает Node/Java thread dump — смотрите nproc в пике.

Не privileged «чтобы лимиты не мешали».

Сумма mem_limit по контейнерам плюс резерв OS (journal, sshd, сам dockerd, page cache) должна быть меньше RAM. Если каждый сервис «по 4 GiB на всякий» на хосте 8 GiB, лимиты не спасут от хостового OOM — они просто очертят, кого убьют первым. Резервирование mem_reservation помогает планировщику, но не заменяет hard limit.

cpus: 0.5 на JVM без понимания GC даст длинные паузы, не «честную долю». Смотрите docker stats CPU %. Для сборки webpack вынесите job, не поднимайте прод-cpus до числа ядер хоста.

pids_limit слишком низкий ломает npm/java (потоки = задачи). Снимите ls /proc/PID/task | wc в пике на стенде, умножьте с запасом. Безлимит pids оставляет fork-бомбу.

Проверка, что YAML применяется: после up Memory в inspect в байтах (536870912 для 512m). Ноль значит, что вы правили deploy.resources на non-swarm и Compose v2 в вашей версии их проигнорировал. Дублируйте mem_limit. Не privileged «чтобы cgroup не давил».

Как проверить, что проблема устранена

docker inspect app-web-1 --format 'mem={{.HostConfig.Memory}} cpus={{.HostConfig.NanoCpus}} pids={{.HostConfig.PidsLimit}}'
docker stats --no-stream app-web-1
docker inspect app-web-1 --format '{{.State.OOMKilled}} {{.State.ExitCode}}'
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/health

Memory ≠ 0, pids ≠ 0/unlimited. Health ок. Под нагрузкой хост не уходит в OOM (смотрите dmesg и RAM хоста). app_data не затронут.

Если не помогло

  • YAML есть, inspect 0: не тот compose file / не recreate / только deploy на non-swarm.
  • Контейнер не стартует: memory меньше минимума JVM (Cannot allocate). Поднимите, не безлимит.
  • pids EAGAIN: мало, увеличьте.
  • Хостовый OOM при всех лимитах: сумма лимитов > RAM, или безлимитный сосед, или сам dockerd/build.

Лимит swap меньше memory на части ядер отвергается daemon; задайте memory-swapmemory или равным, согласно политике «без swap у контейнера».

Профилактика

  • Шаблон compose с memory+cpus+pids обязателен.
  • Алерт OOMKilled.
  • Сумма mem_limit < RAM хоста с запасом под OS/docker.
  • Не собирать образы на прод-хосте.
  • Hardening baseline.

На cgroup v2 файл memory.max со значением max значит безлимит, даже если вы «вроде задали» в YAML. Сверяйте sysfs или inspect, не чувство. После docker update внесите YAML в git в том же окне, иначе следующий деплой снимет лимит.

FAQ

mem_limit vs deploy.resources.limits.memory

На Swarm — deploy. На compose up проверяйте inspect, не веру в спецификацию. Дублировать mem_limit надёжнее, если inspect иначе нули.

Нужен ли cpu_shares?

Это soft. cpus — hard quota. Для соседей на одном хосте hard важнее.

memory-swap -1

Неограниченный swap — путь к thrash хоста. Не надо.

Лимиты и privileged

Privileged контейнер может обходить часть ограничений. Снимите privileged.

Java -Xmx vs mem_limit

-Xmx должен быть ниже лимита cgroup с запасом (metaspace, threads). Иначе OOM killer, не Java heap dump.