Короткий ответ
Без лимитов контейнер 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.
Симптомы и как отличить
dmesgOOM killer наpythonиз контейнера, хост подвис;docker statsMEM 12 GiB при 16 GiB хоста, лимит 0;- сотни процессов
java— нет pids; dbтормозит, когдаwebкомпилирует.
| Поле inspect | 0 / пусто | Задано |
|---|---|---|
| Memory | безлимит | байты |
| NanoCpus / CpuQuota | безлимит | доля CPU |
| PidsLimit | часто 0 | N |
Возможные причины
- YAML без memory/cpus.
- Лимиты только в
deploy:, локальный Compose их игнорирует. docker updateзабыли после recreate.- Privileged / свой cgroup-parent сломал учёт.
- Редко:
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/healthMemory ≠ 0, pids ≠ 0/unlimited. Health ок. Под нагрузкой хост не уходит в OOM (смотрите dmesg и RAM хоста). app_data не затронут.
Если не помогло
- YAML есть, inspect 0: не тот compose file / не recreate / только deploy на non-swarm.
- Контейнер не стартует: memory меньше минимума JVM (
Cannot allocate). Поднимите, не безлимит. pidsEAGAIN: мало, увеличьте.- Хостовый OOM при всех лимитах: сумма лимитов > RAM, или безлимитный сосед, или сам dockerd/build.
Лимит swap меньше memory на части ядер отвергается daemon; задайте memory-swap ≥ memory или равным, согласно политике «без 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.