DevOps и Контейнеризация 8 мин чтения 2026-09-03

Docker на VDS за 400 ₽: как настроить лимиты памяти и CPU в Docker Compose, чтобы один сбойный контейнер не уронил весь сервер

Практическое руководство по жесткой изоляции ресурсов контейнеров в Docker Compose v2. Как предотвратить падение сервера и базы данных из-за утечек памяти (Linux OOM Killer), правильно задать limits и reservations для RAM/CPU, настроить ротацию логов и мониторинг.

Инженерная редакция SysKit Проверено инженерами SysKit.ru

Знакомая картина: вы арендовали компактный VDS за 300–500 рублей в месяц (2 vCPU, 2–4 ГБ RAM), развернули на нём связку из Nginx Proxy Manager, пару сайтов клиентов, бэкенд на Node.js или Python, базу данных MySQL/PostgreSQL и Telegram-бота. Всё работает быстро и ровно, пока однажды в 3 часа ночи не прилетает тревожный алерт: сайты лежат, SSH не отвечает или сервер ушёл в глухой ступор.

Вы заходите в консоль VDS через панель хостинга, перезагружаете сервер, открываете dmesg или journalctl и видите страшную надпись: Out of memory: Kill process (mysqld) score ... or sacrifice child. Один из фоновых контейнеров поймал утечку памяти (memory leak) или зациклился, а ядро Linux принудительно убило самый ценный процесс — базу данных, похоронив работу всех остальных клиентских проектов.

⚠️ Главная иллюзия контейнеризации

По умолчанию Docker не ограничивает оперативную память и процессорное время для контейнеров. Если контейнер запустит бесконечный цикл или выделит 3 ГБ RAM на сервере с 2 ГБ, он без ограничений сожрёт всю память хоста, вызовет панику ядра Linux (OOM) и уронит соседние службы.

1. Анатомия катастрофы: почему при сбое одного контейнера падает весь VDS

В ядре Linux работает защитный механизм — Out of Memory (OOM) Killer. Когда физическая оперативная память и Swap-пространство полностью исчерпаны, ядро оказывается перед выбором: либо полный крах всей операционной системы (Kernel Panic), либо принудительное уничтожение одного из процессов.

Для выбора «жертвы» Linux рассчитывает показатель oom_score (от 0 до 1000). Чем больше оперативной памяти занимает процесс и чем дольше он работает, тем выше его балл. Именно поэтому, когда течет легкий скрипт или фоновый воркер, OOM Killer практически никогда не убивает виновника. Он хладнокровно уничтожает MySQL, PostgreSQL или мастер-процесс PHP-FPM / Nginx, потому что те держат в буферах основной массив памяти.

Результат: виновник утечки продолжает потреблять остатки ресурсов, а сайты клиентов выдают ошибки 502 Bad Gateway или Error establishing a database connection.

2. Синтаксис лимитов в Docker Compose v2: limits vs reservations

Долгое время вокруг лимитов ресурсов в Docker Compose была путаница. В старых версиях Compose v1 директива deploy.resources работала только в режиме Docker Swarm, а при обычном docker-compose up требовалось указывать устаревший флаг --compatibility.

Начиная с Docker Compose v2 (встроенного прямо в CLI docker compose без дефиса), блок deploy.resources стал официальным стандартом и полноценно применяется для любых одиночных серверов. В конфигурации используются два фундаментальных понятия:

  • limits.memory (Hard Limit — жесткий потолок): Абсолютный предел памяти, который ядро cgroups разрешает выделить контейнеру. Если процесс внутри контейнера попытается запросить хотя бы на 1 байт больше этого значения, OOM Killer убьет только этот конкретный контейнер (с кодом выхода 137), а хост-система и соседние контейнеры продолжат работать без малейших сбоев.
  • reservations.memory (Soft Limit — гарантированный минимум): Объем оперативной памяти, который Docker гарантирует контейнеру при запуске. Если на сервере возникнет дефицит ресурсов, ядро не станет отбирать эту зарезервированную память.

💡 Совет инженера: Если вы используете сертифицированные дистрибутивы Ubuntu 22.04 / 24.04 LTS и современный Docker Engine 25+, всегда задавайте жесткий лимит limits.memory для каждого контейнера без исключения. Для второстепенных ботов ставьте от 128M до 256M, для веб-серверов 256M–512M, для баз данных — 512M–1.5G в зависимости от тарифа VDS.

3. Ограничение процессора (CPU): защита от 100% зависаний ядер

Вторая распространенная проблема дешевых серверов — зависший скрипт или регулярное выражение в парсере, которое загружает 1 vCPU на 100%. Из-за этого входящие сетевые пакеты Nginx перестают обрабатываться вовремя, а время отклика (TTFB) подскакивает с 50 мс до 15 секунд.

В блоке deploy.resources.limits параметр cpus задается дробным числом, определяющим доступную долю процессорного времени:

  • cpus: '0.50' — контейнеру разрешено использовать не более 50% мощности одного процессорного ядра.
  • cpus: '1.50' — контейнер может утилизировать до полутора ядер суммарно на многоядерном сервере.

Для фоновых воркеров, ботов и сборщиков логов никогда не оставляйте неограниченный CPU: лимит 0.500.75 гарантирует, что даже при глубоком зацикливании сервер сохранит отзывчивость по SSH и продолжит отдавать статику в вебе.

4. Готовый эталонный docker-compose.yml с изоляцией ресурсов

Ниже представлен готовый боевой шаблон docker-compose.yml для типичного VDS с 2 vCPU и 4 ГБ RAM. В нем развернуты 4 изолированных сервиса: обратный прокси Nginx, веб-приложение, фоновый Telegram-бот и база данных MariaDB/MySQL.

version: '3.8'

services:
  # 1. Обратный прокси-сервер (Nginx)
  reverse-proxy:
    image: nginx:alpine
    container_name: prod_proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    deploy:
      resources:
        limits:
          cpus: '0.75'
          memory: 256M
        reservations:
          memory: 64M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - web_net

  # 2. Основное веб-приложение (Node.js / Python / PHP)
  web-app:
    image: node:20-alpine
    container_name: prod_webapp
    restart: unless-stopped
    command: ["node", "server.js"]
    deploy:
      resources:
        limits:
          cpus: '1.00'
          memory: 768M
        reservations:
          memory: 256M
    logging:
      driver: "json-file"
      options:
        max-size: "15m"
        max-file: "3"
    depends_on:
      - db
    networks:
      - web_net
      - internal_net

  # 3. Фоновый Telegram-бот или воркер
  tg-bot:
    image: python:3.12-slim
    container_name: prod_bot
    restart: on-failure:5
    deploy:
      resources:
        limits:
          cpus: '0.40'
          memory: 256M
        reservations:
          memory: 64M
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
    networks:
      - internal_net

  # 4. База данных (MariaDB 11 / MySQL 8)
  db:
    image: mariadb:11
    container_name: prod_db
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: "StrongGeneratedPassword_2026!"
      MYSQL_DATABASE: "production_db"
    volumes:
      - db_data:/var/lib/mysql
    deploy:
      resources:
        limits:
          cpus: '1.20'
          memory: 1200M
        reservations:
          memory: 512M
    logging:
      driver: "json-file"
      options:
        max-size: "20m"
        max-file: "3"
    networks:
      - internal_net

volumes:
  db_data:

networks:
  web_net:
    driver: bridge
  internal_net:
    driver: bridge

Суммарный жесткий лимит всех контейнеров в этом примере: 256M + 768M + 256M + 1200M = 2480M (~2.4 ГБ). Это идеальный баланс для VDS с 4 ГБ памяти: у операционной системы хоста, демона Docker, SSHD и дискового кэша остается гарантированный буфер в 1.5 ГБ RAM, что полностью исключает падение ядра.

5. Вторая главная боль: защита диска от переполнения логами (json-file)

Вторая по популярности причина внезапной гибели VDS — переполнение диска логами Docker (/var/lib/docker/containers/.../*-json.log). По умолчанию Docker сохраняет весь стандартный вывод контейнеров без ограничения размера. Если скрипт начинает спамить ошибками соединения в stdout, за пару суток файл лога может разрастись до 20–40 ГБ, диск забивается до 100% (ошибка No space left on device), и Docker аварийно завершает работу.

Чтобы раз и навсегда решить эту проблему на уровне всего сервера, настройте глобальный конфиг демона Docker /etc/docker/daemon.json:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

После сохранения примените настройки перезапуском службы: sudo systemctl restart docker. Теперь ни один контейнер на сервере физически не сможет занять под логи более 30 МБ (3 файла по 10 МБ с автоматической ротацией и удалением старых записей).

6. Стратегии перезапуска: почему restart: always опасен на дешевом VDS

Многие вебмастера машинально пишут для всех сервисов директиву restart: always. На недорогом VDS с ограниченными ресурсами это скрытая бомба замедленного действия:

Политика restart Поведение при падении Где использовать
unless-stopped Перезапускается всегда, кроме случая, когда контейнер был остановлен вручную командой docker stop. Критически важные сервисы (Nginx, База данных, основной Web).
on-failure:5 Перезапускается только при ненулевом коде выхода (падение, OOM) максимум 5 раз. Если падает постоянно — останавливается. Telegram-боты, парсеры, воркеры очередей, интеграционные скрипты.
always Перезапускается бесконечно. При фатальной ошибке запускает бесконечный цикл (bootloop), утилизируя CPU хоста. Не рекомендуется для фоновых скриптов на слабых серверах.

Если в вашем Telegram-боте произойдет фатальный синтаксический сбой или отвалится внешний API, политика on-failure:5 предотвратит цикличный бесконечный перезапуск и сохранит ресурсы сервера для стабильной работы клиентских сайтов.

7. Диагностика в реальном времени: docker stats и быстрый поиск утечек

Чтобы в любой момент увидеть реальную картину потребления ресурсов каждым контейнером, выполните в терминале хоста команду:

docker stats --no-stream --format "table {{.Name}}	{{.CPUPerc}}	{{.MemUsage}}	{{.MemPerc}}	{{.NetIO}}"

Вывод отобразит лаконичную таблицу с точными процентами использования выделенной памяти и CPU:

NAME           CPU %     MEM USAGE / LIMIT     MEM %     NET I/O
prod_db        1.25%     412.5MiB / 1.172GiB   34.35%    18.4MB / 12.1MB
prod_webapp    0.45%     184.2MiB / 768MiB     23.98%    42.6MB / 95.3MB
prod_proxy     0.10%     24.8MiB / 256MiB      9.69%     152MB / 164MB
prod_bot       0.05%     68.1MiB / 256MiB      26.60%    2.4MB / 1.8MB

Если вы видите, что показатель MEM % приближается к 90–95%, значит приложение накапливает данные без очистки сборщиком мусора, либо размер буфера не соответствует реальному объему трафика. Это сигнал заблаговременно провести профилирование кода до срабатывания OOM Killer.

Для постоянного автоматического мониторинга доступности сайтов и замера времени ответа (TTFB) подключите официального Telegram-бота платформы: @syskit_uptime_bot — он уведомит вас о любых сбоях HTTP и истечении SSL-сертификатов за считанные секунды.

⚡ Бесплатный мониторинг сайтов и серверов в Telegram

Добавьте свои домены и контейнеры в бота @syskit_uptime_bot за 1 минуту: получайте мгновенные алерты о простоях и держите аптайм под полным контролем 24/7.

8. Резюме и чек-лист стабильного Docker-сервера

Развертывание множества проектов на одном доступном VDS — отличная стратегия оптимизации бюджета, если инфраструктура настроена профессионально. Краткий чек-лист надежности:

  • Всегда указывайте deploy.resources.limits.memory для каждого контейнера в docker-compose.yml.
  • Ограничивайте CPU (например cpus: '0.50') для некритичных скриптов и ботов.
  • Настройте ротацию логов через json-file с лимитом max-size: 10m в /etc/docker/daemon.json.
  • Используйте restart: on-failure:5 для ботов вместо слепого restart: always.
  • Оставляйте хост-системе запас минимум в 1–1.5 ГБ неразмеченной оперативной памяти под ядро и системный кэш.

🚀 Нужен надежный сервер под боевой Docker-стек?

Для комфортной работы контейнеров выбирайте проверенные VDS с быстрыми накопителями NVMe и чистой изоляцией ресурсов:

9. Часто задаваемые вопросы (FAQ)

Рекомендуемые VDS-провайдеры

Отказоустойчивые сервера с быстрыми NVMe и каналом до 1 Гбит/с

Мощные процессоры AMD EPYC и NVMe

Timeweb Cloud VDS

Сверхбыстрые NVMe диски до 3000 МБ/с, идеальны для запуска Docker Compose с базами данных и веб-сервисами. Почасовая оплата и моментальные снапшоты перед деплоем.

Конфигурация
от 1 vCPU / 2 GB RAM / 30 GB NVMe
Премиум каналы и надежность Tier III
🔷

Selectel Cloud VPS

Выделенные KVM-ресурсы с гарантированной частотой процессоров. Чистая изоляция ядер и честные лимиты без оверселлинга для продуктивных контейнеров.

Конфигурация
от 1 vCPU / 2 GB RAM / 25 GB NVMe
Автобэкапы и предустановленный Docker
🟠

Beget VPS

Готовый шаблон с предустановленным Docker Engine в 1 клик. Бесплатные ежедневные резервные копии всего VDS и круглосуточная поддержка сисадминов.

Конфигурация
от 1 vCPU / 2 GB RAM / 35 GB NVMe

Вопросы и ответы (FAQ)

Популярные вопросы вебмастеров и сисадминов по данной теме

Что происходит с контейнером, когда он превышает limits.memory?
Ядро Linux через подсистему контрольных групп (cgroups) немедленно отправляет процессу внутри контейнера сигнал SIGKILL (Out of Memory). Контейнер завершает работу с кодом выхода 137. При этом сам сервер хоста и все остальные запущенные контейнеры продолжают работать абсолютно штатно.
Работает ли директива deploy.resources в обычном Docker Compose v2 без Swarm?
Да, в современном Docker Compose v2 (команда docker compose без дефиса) блок deploy.resources.limits полностью поддерживается для одиночных хостов по умолчанию и не требует устаревших флагов compatibility.
Поможет ли создание Swap-файла на VDS, если не хватает оперативной памяти?
Swap-файл (подкачка) на 1–2 ГБ полезен как подушка безопасности от кратковременных всплесков памяти. Однако постоянная работа Docker-контейнеров в Swap на дешевом VDS приведет к резкому падению дискового I/O, скачку iowait процессора до 80-100% и зависанию сайтов. Базам данных и Node.js всегда требуется физическая память RAM.

Читайте также в блоге SysKit

Смежные руководства и полезные технические статьи