Лимиты ресурсов, healthcheck-проверки и политики перезапуска в Docker Compose

10 мин чтения·Matthieu·vpsproductioncontainersdocker-composedocker|

Твой Compose-файл работает в dev, но к продакшну не готов. Разбираемся, как добавить лимиты памяти/CPU, healthcheck-проверки, политики перезапуска и порядок запуска, чтобы защитить VPS от OOM kill и каскадных сбоев.

Compose-файл, который отрабатывает docker compose up без ошибок, ещё не готов к продакшну. Без лимитов ресурсов один контейнер может сожрать всю память хоста и запустить Linux OOM killer, уложив каждый сервис на VPS. Без healthcheck-проверок Docker не сможет обнаружить зависший процесс. Без политик перезапуска упавший контейнер останется мёртвым, пока ты сам не заметишь.

Эти три системы работают вместе: лимиты ресурсов предотвращают неконтролируемое потребление, healthcheck-проверки обнаруживают сбои, а политики перезапуска обеспечивают восстановление. Это руководство рассматривает все три как единый слой продакшн-хардеринга для Docker Compose v2.

Что нужно заранее: рабочая установка Docker Compose на VPS. Знакомство с базовой структурой Compose-файла. Если нужно освежить память, см. .

Как задать лимиты памяти и CPU в Docker Compose?

Используй ключ deploy.resources.limits в определении сервиса. Установи memory в значение вроде 512M или 1G для жёсткого потолка. Установи cpus в десятичную строку вроде '0.5' для половины ядра. Процесс контейнера убивается OOM killer при превышении лимита памяти. CPU-лимиты троттлят процесс, а не убивают его.

services:
  api:
    image: node:22-slim
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

Что это делает: контейнер api может использовать максимум 1 CPU-ядро и 512 МБ RAM. Docker гарантирует минимум 0,25 ядра и 256 МБ, даже когда другие контейнеры борются за ресурсы.

Проверь, что лимиты применились:

docker compose up -d
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

Столбец MEM USAGE / LIMIT показывает и текущее потребление, и настроенный потолок. Ты должен видеть 512MiB как лимит, а не всю RAM хоста. Если видишь полный объём памяти хоста, лимиты не работают.

В чём разница между limits и reservations?

Limits — жёсткие потолки. Контейнер не может их превысить. Reservations — мягкие гарантии. Docker использует их для решений по планированию и обработки давления на память.

Настройка Что делает Когда важно
limits.memory Жёсткий потолок. OOM kill при превышении. Всегда. Защищает от контейнеров, вышедших из-под контроля.
limits.cpus Троттлинг. Процесс замедляется. CPU-интенсивные нагрузки (сборки, инференс).
reservations.memory Гарантированный минимум. Хост под давлением по памяти.
reservations.cpus Гарантированная доля CPU. Несколько CPU-интенсивных контейнеров.

Без reservations Docker раздаёт ресурсы по принципу «кто первый пришёл». Под нагрузкой любой контейнер может остаться ни с чем. Задавай reservations равными минимуму, который сервису нужен для работы.

Что происходит, когда Docker-контейнер превышает лимит памяти?

OOM killer ядра Linux завершает основной процесс контейнера через SIGKILL. Docker фиксирует код выхода 137 (128 + 9, где 9 — SIGKILL). Если политика перезапуска on-failure или always, Docker перезапускает контейнер автоматически.

Проверь, был ли контейнер убит OOM killer:

docker inspect api-1 --format '{{.State.OOMKilled}}'

Вывод: true подтверждает OOM kill.

Для подробностей:

docker inspect api-1 --format '{{json .State}}' | python3 -m json.tool

Ищи "OOMKilled": true, "ExitCode": 137 и "RestartCount", чтобы узнать, сколько раз контейнер перезапускался.

Без лимитов контейнер выделяет память, пока хост не опустеет. Ядро начинает убивать процессы по всей системе, чтобы освободить RAM. Это может положить твою базу данных, реверс-прокси или SSH-демон. Лимиты ограничивают зону поражения проблемным контейнером.

Как спланировать бюджет ресурсов контейнеров на VPS?

На VPS с фиксированными ресурсами нужно распределить память между всеми контейнерами. Оставь запас для хостовой ОС и самого Docker.

Пример бюджета для VPS с 8 ГБ:

Компонент Память
Хостовая ОС + Docker engine 1 ГБ
PostgreSQL 2 ГБ
Redis 512 МБ
Node.js API 1 ГБ
Nginx 128 МБ
Фоновый воркер 1 ГБ
Запас (нераспределённый) 2,35 ГБ

Держи 20-30% нераспределёнными как запас. Если сумма лимитов контейнеров превышает общий объём RAM хоста, рискуешь тем, что хостовый OOM killer вмешается, а он игнорирует границы контейнеров Docker.

Сверь общее распределение с памятью хоста:

free -h
docker stats --no-stream --format "{{.Name}}: {{.MemUsage}}"

Как настроить healthcheck в Docker Compose?

Добавь блок healthcheck в определение сервиса. Docker запускает тестовую команду с заданным интервалом и помечает контейнер как unhealthy после настроенного количества последовательных неудач. Другие сервисы могут зависеть от этого статуса здоровья для определения порядка запуска.

services:
  api:
    image: node:22-slim
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
      start_interval: 2s

Параметры healthcheck:

Параметр По умолчанию Рекомендуется Что контролирует
interval 30s 15-60s Время между проверками после запуска
timeout 30s 5-10s Макс. время на одну проверку
retries 3 3-5 Сбоев до unhealthy
start_period 0s 10-60s Льготный период для медленно стартующих сервисов
start_interval 5s 2-5s Интервал во время запуска (Compose v2.20+)

Параметр start_interval (появился в Compose v2.20) позволяет проверять чаще во время запуска. Контейнер переходит из starting в healthy сразу после первой успешной проверки в течение start_period. Далее проверки идут с обычным interval.

В чём разница между CMD и CMD-SHELL в healthcheck?

CMD выполняет команду напрямую, без shell. CMD-SHELL пропускает её через /bin/sh -c. По возможности используй CMD. Это убирает оверхед shell и устраняет проблемы с PID 1, когда shell, а не твоя команда проверки, получает сигналы.

# Формат CMD - без shell, запускает бинарник напрямую
healthcheck:
  test: ["CMD", "pg_isready", "-U", "postgres"]

# Формат CMD-SHELL - через /bin/sh -c
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]

# Строковое сокращение - эквивалент CMD-SHELL
healthcheck:
  test: curl -f http://localhost:3000/health || exit 1

Используй CMD-SHELL, когда нужны фичи shell: ||, пайпы или раскрытие переменных. CMD — для простого запуска бинарников.

Какой healthcheck использовать для PostgreSQL, Redis и Nginx?

Каждому сервису нужен healthcheck, который проверяет способность обрабатывать запросы, а не просто работает ли процесс.

Сервис Команда healthcheck Что проверяет
PostgreSQL ["CMD", "pg_isready", "-U", "postgres"] Принимает соединения
Redis ["CMD", "redis-cli", "ping"] Отвечает на команды
Nginx `["CMD-SHELL", "curl -f http://localhost/
Node.js-приложение `["CMD-SHELL", "curl -f http://localhost:3000/health
MySQL/MariaDB ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] Движок готов, не просто сокет открыт

Важно: для healthcheck на базе curl образ должен содержать curl. Многие slim-образы его не имеют. Установи в Dockerfile, используй wget вместо него или напиши минимальный эндпоинт здоровья, который твой фреймворк отдаёт нативно.

Проверь статус healthcheck:

docker compose ps

Ищи (healthy) или (unhealthy) в столбце STATUS. Для детальной истории:

docker inspect api-1 --format '{{json .State.Health}}' | python3 -m json.tool

Здесь видны последние результаты проверок, включая stdout/stderr вывод неудачных. Обрати внимание: если FailingStreak продолжает расти, твоя команда проверки неправильная или сервис действительно сломан.

Как политики перезапуска взаимодействуют с healthcheck?

Политики перезапуска управляют тем, что Docker делает при остановке контейнера. Healthcheck управляют обнаружением проблем в работающих контейнерах. Вместе они образуют цикл автоматического восстановления: healthcheck обнаруживает сбой, контейнер останавливается, политика перезапуска поднимает его обратно.

services:
  api:
    restart: on-failure:5
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

Сравнение политик перезапуска

Политика При крэше При ребуте При docker stop Подходит для
no Остаётся мёртвым Остаётся мёртвым Остаётся мёртвым Одноразовые задачи, отладка
always Перезапуск Перезапуск Перезапуск Базовая инфраструктура (БД, прокси)
unless-stopped Перезапуск Перезапуск Остаётся мёртвым Большинство продакшн-сервисов
on-failure:N Перезапуск (до N раз) Остаётся мёртвым Остаётся мёртвым Сервисы, которые не должны перезапускаться вечно

on-failure:5 значит, что Docker перезапускает до 5 раз. Потом контейнер остаётся мёртвым. Это предотвращает циклы перезапуска, которые тратят CPU на принципиально сломанный сервис.

Взаимодействие OOM + перезапуск: когда контейнер достигает лимита памяти и получает OOM kill (код выхода 137), Docker считает это сбоем. С on-failure:5 перезапускает до 5 раз. Если сервис утекает по памяти, он будет убит и перезапущен многократно до достижения лимита попыток. Проверь счётчик перезапусков:

docker inspect api-1 --format '{{.RestartCount}}'

Для большинства продакшн-сервисов бери unless-stopped. Он перезапускает при крэшах и ребутах хоста, но уважает ручные команды docker compose stop. Используй on-failure:N для сервисов, где цикл крэшей должен вызвать алерт, а не молча перезапускаться вечно.

Как заставить сервис ждать, пока другой станет здоровым?

Используй depends_on с condition: service_healthy. Docker Compose будет ждать прохождения healthcheck зависимости перед запуском зависимого сервиса.

services:
  db:
    image: postgres:17
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  api:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
    restart: unless-stopped

Без condition: service_healthy depends_on ждёт только запуска контейнера, а не готовности сервиса внутри. PostgreSQL тратит несколько секунд на инициализацию. Приложение упадёт, пытаясь подключиться к базе, которая ещё не принимает соединения.

Опция restart: true внутри depends_on (Compose v2.21+) говорит Docker перезапустить зависимый сервис, если зависимость перезапускается:

depends_on:
  db:
    condition: service_healthy
    restart: true

Полезно, когда приложение кэширует соединение с БД и не может восстановиться после перезапуска базы без полного собственного перезапуска.

Какие ulimits выставлять для продакшн-контейнеров?

Задай nofile (открытые файловые дескрипторы) и nproc (макс. количество процессов) для сервисов с большим количеством одновременных соединений. Каждое TCP-соединение, открытый файл и пайп потребляет файловый дескриптор. Лимит по умолчанию (1024 во многих образах) слишком мал для баз данных и высоконагруженных сервисов.

services:
  db:
    image: postgres:17
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
      nproc:
        soft: 4096
        hard: 4096

Проверка внутри контейнера:

docker compose exec db cat /proc/1/limits

Ищи Max open files и Max processes. Значения должны совпадать с заданными.

Защита от fork bomb с лимитом PID

Задай deploy.resources.limits.pids, чтобы ограничить количество процессов, которые контейнер может создать. Это защитит от fork bomb и неконтролируемого порождения процессов, которые могут исчерпать все PID на хосте.

services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          pids: 200

Если ты не используешь deploy.resources для лимитов CPU/памяти, ключ верхнего уровня pids_limit тоже работает. Но когда deploy.resources.limits присутствует, лимит PID тоже должен быть там. Смешивание обоих вариантов вызывает ошибку валидации в Compose v5+.

200 PID — это щедро для типичного веб-приложения. Node.js-приложение использует примерно 10-30. PostgreSQL расходует примерно один процесс на соединение плюс фоновые воркеры. Закладывай 2-3x от ожидаемого пика.

Как ограничить размер логов контейнеров?

Без лимитов логи контейнеров растут бесконечно. Болтливый сервис может забить диск за несколько часов. Задай max-size и max-file в драйвере логирования json-file для автоматической ротации логов.

services:
  api:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Это хранит максимум 3 файла по 10 МБ каждый, ограничивая хранение логов до 30 МБ на сервис. Подгони под свои потребности в отладке. Используй YAML-якорь, чтобы применить одну конфигурацию логирования ко всем сервисам:

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

services:
  api:
    image: myapp:latest
    logging: *default-logging
  worker:
    image: myapp:latest
    logging: *default-logging

Настройка stop_grace_period для корректного завершения

Когда Docker останавливает контейнер, он отправляет SIGTERM и ждёт корректного завершения процесса. Если процесс не завершился в течение льготного периода, Docker отправляет SIGKILL. По умолчанию — 10 секунд.

services:
  db:
    image: postgres:17
    stop_grace_period: 30s

  api:
    image: myapp:latest
    stop_grace_period: 5s

Базам данных нужны более длинные льготные периоды, чтобы сбросить записи и корректно закрыть соединения. Веб-серверы и API-процессы обычно завершаются за пару секунд. Выставляй льготный период по реальному времени завершения сервиса с небольшим запасом.

Полный продакшн-готовый Compose-файл

Этот пример объединяет все настройки для типичного стека веб-приложения: реверс-прокси Nginx, API Node.js, база данных PostgreSQL и кэш Redis.

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 128M
        reservations:
          memory: 64M
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging
    depends_on:
      api:
        condition: service_healthy

  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
          pids: 200
        reservations:
          cpus: '0.25'
          memory: 256M
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
      start_interval: 2s
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 1G
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 15s
    restart: unless-stopped
    stop_grace_period: 30s
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    logging: *default-logging
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          memory: 128M
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging

volumes:
  pgdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt

Обрати внимание: пароль базы данных использует Docker secrets через POSTGRES_PASSWORD_FILE вместо текстовой переменной окружения POSTGRES_PASSWORD. Создай файл секрета с ограниченными правами:

mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txt

Чеклист проверки

После применения всех настроек пройди эти проверки, чтобы убедиться, что всё активно.

1. Проверить, что лимиты ресурсов применены:

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

Столбец MEM USAGE / LIMIT показывает текущее потребление и настроенный потолок. Каждый контейнер должен показывать свой настроенный лимит памяти, а не общий RAM хоста.

2. Проверить статус healthcheck:

docker compose ps

Все сервисы должны показывать (healthy) в столбце STATUS. Если какие-то показывают (health: starting), подожди окончания start_period.

3. Проверить политику перезапуска:

docker inspect --format '{{.HostConfig.RestartPolicy.Name}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}}' $(docker compose ps -q)

4. Проверить ulimits внутри контейнера:

docker compose exec db cat /proc/1/limits | grep -E "open files|processes"

5. Проверить конфигурацию логов:

docker inspect --format '{{.HostConfig.LogConfig.Type}} max-size={{index .HostConfig.LogConfig.Config "max-size"}} max-file={{index .HostConfig.LogConfig.Config "max-file"}}' $(docker compose ps -q api)

6. Протестировать полную цепочку восстановления:

Останови контейнер и наблюдай за восстановлением:

docker compose stop api
docker compose ps  # api должен показывать Exited
docker compose start api
docker compose ps  # api должен показывать (healthy) после start_period
docker inspect $(docker compose ps -q api) --format 'RestartCount: {{.RestartCount}}'

Чтобы протестировать автоматический перезапуск при реальном крэше, опусти лимит памяти ниже минимума приложения. Политика перезапуска срабатывает, когда процесс завершается сам. Учти, что docker kill не вызывает политики перезапуска в свежих версиях Docker.

Краткий справочник по размерам ресурсов

Стартовые значения для распространённых сервисов на VPS. Подгоняй под реальную нагрузку.

Сервис Лимит памяти Лимит CPU Healthcheck
PostgreSQL 1-4 ГБ 1.0-2.0 pg_isready -U postgres
Redis 256M-1G 0.25-0.5 redis-cli ping
Node.js API 256M-1G 0.5-1.0 curl -f http://localhost:PORT/health
Nginx 64M-256M 0.25-0.5 curl -f http://localhost/
Ollama (LLM) 4-8 ГБ 2.0-4.0 curl -f http://localhost:11434/
Фоновый воркер 256M-1G 0.5-1.0 Проверка, специфичная для приложения

Что-то пошло не так?

Контейнер постоянно перезапускается (цикл перезапуска):

docker compose logs api --tail 50
docker inspect api-1 --format '{{.State.ExitCode}} OOM:{{.State.OOMKilled}} Restarts:{{.RestartCount}}'

Если OOMKilled: true, увеличь лимит памяти. Если код выхода не 137, проверь логи приложения на предмет реальной ошибки.

Healthcheck всегда фейлится:

docker inspect api-1 --format '{{json .State.Health.Log}}' | python3 -m json.tool

Показывает вывод каждой проверки. Частые причины: эндпоинт здоровья не существует, curl не установлен в образе, или сервис слушает на другом порту, чем указано в проверке.

depends_on не ждёт:

Убедись, что у зависимости определён healthcheck. Без него condition: service_healthy нечего ждать, и Compose выдаст ошибку при старте.

Лимиты не отображаются в docker stats:

Проверь, что используешь Docker Compose v2 (плагин docker compose, а не старый бинарник docker-compose). Проверь версию:

docker compose version

Ключ deploy.resources требует Compose v2. Если на старой версии, см. для инструкций по установке.

Чтение логов при сбоях:

journalctl -u docker -f
docker compose logs -f --tail 100

Лог демона Docker показывает OOM-события и изменения жизненного цикла контейнеров. Логи Compose показывают вывод приложения.


Авторское право 2026 Virtua.Cloud. Все права защищены. Данный контент является оригинальным произведением команды Virtua.Cloud. Воспроизведение, повторная публикация или распространение без письменного разрешения запрещены.

Готовы попробовать?

Разверните свой сервер за секунды. Linux, Windows или FreeBSD.

Смотреть тарифы VPS