В типичном продакшн‑проекте сегодня легко оказаться в ситуации «всё сразу на одном VDS»: PHP‑монолит или несколько микросервисов, пара Node.js‑приложений для API и WebSocket, очереди на RabbitMQ или Redis, крон‑задачи, воркеры, вспомогательные демоны. Если этим не управлять системно, через пару месяцев сервер превращается в зоопарк из случайных скриптов и неясных перезапусков.
Современный Linux уже даёт все строительные блоки для аккуратной организации такой инсталляции: systemd, cgroup v2, юниты и таймеры, watchdog, ограничения CPU/RAM/IO. При грамотной схеме они полностью заменяют связки вроде supervisord плюс самописные bash‑обёртки, а вы получаете управляемость и предсказуемое поведение под нагрузкой.
Когда PHP и Node.js живут на одном VDS
Сценарий, который часто встречается у админов и devops:
- PHP (обычно через php-fpm) обслуживает основной веб‑сайт или API.
- Node.js крутит real‑time: WebSocket, Socket.IO, очереди задач, генерацию превью и т.п.
- Брокер очередей — RabbitMQ или Redis (иногда оба).
- Отдельные воркеры для фоновки: обработка писем, отчёты, импорты.
Технологический стек разный, но для ОС это всего лишь набор процессов. Главные задачи администратора:
- чётко определить, какие демоны должны стартовать всегда;
- защитить систему от падения из‑за утечки памяти в одном сервисе;
- обеспечить корректный перезапуск и остановку при деплоях;
- собрать логи так, чтобы можно было ревьюить инциденты.
Для этого и существует связка systemd + cgroup v2. Никаких дополнительных «общих управляющих демонов» вроде supervisord, как правило, уже не нужно.
Почему systemd, а не supervisord
supervisord долгое время был де‑факто стандартом в Python/Node‑мире для перезапуска процессов. Сейчас его ключевые плюсы почти полностью перекрываются возможностями systemd:
- unit‑файлы дают декларативное описание сервиса — меньше шансов «сломать» конфиг;
- restart‑политики в systemd гибкие и понятные (on-failure, on-abort, always и т.д.);
- интеграция с cgroup v2 позволяет задавать лимиты ресурсов для каждого сервиса;
- централизованные логи через journalctl;
- таймеры, watchdog, socket‑activation и другие плюшки из коробки.
supervisord всё ещё может быть полезен внутри контейнеров без systemd, но на полноценном VDS лучше опираться на нативные механизмы ОС. Это снижает количество подвижных частей и упрощает поддержку конфигурации.

Базовая структура сервисов на одном VDS
Прежде чем писать первый unit‑файл, стоит ответить себе, какие группы сервисов у вас есть:
- веб‑фронтенды: nginx или другой HTTP‑сервер, php-fpm, Node.js API;
- очереди и кеш: Redis, RabbitMQ;
- фоновка: PHP/Node.js‑воркеры, cron‑джобы, потребители из очередей;
- вспомогательное: миграторы БД, indexer‑ы, email‑отправители.
Удобный подход — разбить их на systemd слайсы и сервисы так, чтобы можно было:
- перезагружать PHP‑часть, не трогая Node.js и очереди;
- ограничивать ресурсы фоновки отдельно от фронтендов;
- разделять сервисы из разных проектов (мульти‑тенантный VDS).
Практическое правило: один демон — один systemd‑сервис. Не запускайте в одном юните по несколько процессов, кроме специально задуманных master/worker‑схем, как у php-fpm или nginx.
Подробно про деление PHP‑сервисов по слайсам и пулам мы разбирали в материале про systemd, cgroup и php-fpm — его можно использовать как дополнение к этой статье.
Короткий обзор cgroup v2: что нам действительно нужно
cgroup v2 — это единая иерархия групп ресурсов с аккуратной моделью, в отличие от «зоопарка контроллеров» v1. systemd плотно интегрирован с cgroup v2: каждый сервис живёт в отдельной группе, а лимиты на CPU, память, IO задаются через параметры юнитов.
Из всего многообразия контроллеров нас чаще всего интересуют:
- память:
MemoryMax,MemoryHigh,MemorySwapMax; - CPU:
CPUQuota,CPUWeight; - IO (blkio/io):
IOReadBandwidthMax,IOWriteBandwidthMax(на новых системах — через io‑контроллер).
Вместо ручной работы с /sys/fs/cgroup мы пользуемся только директивами systemd. Это и проще, и менее ломко при обновлениях дистрибутива.
Типовая конфигурация: PHP‑сайт + Node.js + Redis + RabbitMQ
Рассмотрим условный, но очень жизненный пример:
- основной сайт на PHP (nginx + php-fpm);
- Node.js‑сервис для WebSocket и фоновой обработки задач;
- Redis для кеша и быстрых очередей;
- RabbitMQ как «тяжёлый» брокер с подтверждениями для критичных задач.
На уровне systemd это может выглядеть как набор сервисов:
nginx.service;php-fpm.service(илиphp8.3-fpm.service— зависит от дистрибутива);redis.service;rabbitmq-server.service;node-api.service— REST/GraphQL API;node-ws.service— WebSocket‑сервер;php-worker.service— воркеры очередей на PHP;node-worker.service— тяжёлые воркеры на Node.js.
Каждый из них — отдельный unit с чёткими лимитами и политикой перезапуска.
Пример systemd‑юнита для Node.js‑сервиса
Начнём с простого юнита для Node.js API. Допустим, наше приложение запускается командой node /opt/app/api/server.js.
[Unit]
Description=Node.js API service
After=network.target redis.service rabbitmq-server.service
Requires=network.target
[Service]
Type=simple
WorkingDirectory=/opt/app/api
ExecStart=/usr/bin/node server.js
User=app
Group=app
Restart=on-failure
RestartSec=5
# Логи в journald
StandardOutput=journal
StandardError=journal
# Ограничения ресурсов (cgroup v2 через systemd)
MemoryMax=512M
CPUQuota=50%
# Безопасность (базовый минимум)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
Важные моменты:
Restart=on-failure— не перезапускать на штатной остановке, только при краше;After=redis.service rabbitmq-server.serviceгарантирует, что API не стартует раньше брокеров;MemoryMaxиCPUQuotaзащищают VDS в случае, если Node.js внезапно «потечёт».
Пример юнита для PHP‑воркеров
У PHP‑воркеров (например, Laravel Horizon или собственный консольный потребитель очередей) логика похожа: нам нужен один master‑процесс, который сам управляет пулом воркеров внутри.
[Unit]
Description=PHP queue worker
After=network.target redis.service rabbitmq-server.service
Requires=network.target
[Service]
Type=simple
WorkingDirectory=/opt/app/php
ExecStart=/usr/bin/php artisan queue:work --tries=3 --sleep=1
User=app
Group=app
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
MemoryMax=256M
CPUQuota=30%
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
Типичный вопрос на продакшене: как не допустить, чтобы очереди «съели» все ресурсы? Ответ — жёстко ограничивать воркеры по памяти и CPU, а при необходимости поднимать больше экземпляров на дополнительных серверах, а не раздувать один до бесконечности. Детальнее схему воркеров под systemd мы разбирали в статье про очереди и замену supervisord.
Сервисы очередей: Redis и RabbitMQ
Redis и RabbitMQ обычно ставятся из пакетов дистрибутива и уже имеют unit‑файлы. Но ничто не мешает вам добавить оверрайд с лимитами ресурсов через systemctl edit.
Пример оверрайда для Redis:
[Service]
MemoryMax=1G
CPUQuota=40%
Точно так же можно ограничить RabbitMQ. Главное — не задавить их слишком маленькими лимитами, иначе под пиковой нагрузкой вы получите не отказоустойчивость, а cascade failure.
Группировка сервисов через systemd slices
Если на одном VDS крутится несколько проектов, удобно группировать их через systemd slices. Например:
project-a.slice— все юниты проекта A;project-b.slice— все юниты проекта B.
Слайс — это по сути директория cgroup с общими лимитами. Вы можете ограничить целый проект, а внутри уже делить ресурсы между сервисами.
Пример определения слайса для одного проекта:
[Unit]
Description=Project A slice
[Slice]
MemoryMax=4G
CPUQuota=200%
Далее, в юнитах проекта вы добавляете:
[Service]
Slice=project-a.slice
Таким образом, даже если один сервис внутри проекта вышел из берегов, общий лимит слайса защитит остальные сервисы и соседние проекты.
Миграция с supervisord на systemd
Во многих наследованных проектах всё ещё крутятся Node.js и PHP‑воркеры через supervisord. Миграция обычно проходит по шаблону:
- Снять текущее описание процессов из
supervisord.conf: что запускается, с какими аргументами. - Для каждого процесса создать отдельный unit‑файл в
/etc/systemd/system. - Выставить аналогичные рестарт‑политики и рабочую директорию.
- Добавить лимиты по памяти и CPU (то, чего в supervisord часто не было вовсе).
- Постепенно отключать supervisord и включать новые сервисы через systemd.
Необязательно переписывать всё за один раз. Можно вынести сперва фоновые воркеры, затем WebSocket‑сервисы, а в конце — вспомогательные демоны.
systemd, крон и таймеры
Многие фоновые задачи в PHP и Node.js по привычке вешают на cron. systemd‑таймеры обычно удобнее:
- логика запуска и перезапуска в одном месте с сервисом;
- логи в journald, легче дебажить;
- чёткий контроль зависимостей (например, запускать задачу только когда доступна БД или очередь);
- тонкая настройка пропуска задач при простоях.
Типичный пример: периодический консольный запуск планировщика задач на PHP.
[Unit]
Description=PHP scheduled task
[Service]
Type=oneshot
WorkingDirectory=/opt/app/php
ExecStart=/usr/bin/php artisan schedule:run
User=app
Group=app
[Install]
WantedBy=multi-user.target
Таймер к нему:
[Unit]
Description=Run PHP scheduled task every minute
[Timer]
Unit=php-schedule.service
[Install]
WantedBy=timers.target
В результате вы получаете более прозрачную замену cron‑записей для задач, связанных с PHP и Node.js.
Связка Node.js и PHP через очереди: Redis или RabbitMQ
Когда в одной системе живут PHP и Node.js, часто они общаются через очередь:
- PHP складывает задания в Redis или RabbitMQ;
- Node.js поднимает воркеры‑консьюмеры;
- результат кладётся обратно в кеш или БД.
На уровне systemd для этих воркеров нужен отдельный unit, чтобы не смешивать их с основным API или фронтендом. Например, для Node.js‑воркера:
[Unit]
Description=Node.js queue worker
After=network.target redis.service rabbitmq-server.service
Requires=redis.service
[Service]
Type=simple
WorkingDirectory=/opt/app/node-worker
ExecStart=/usr/bin/node worker.js
User=app
Group=app
Restart=always
RestartSec=2
StandardOutput=journal
StandardError=journal
MemoryMax=256M
CPUQuota=20%
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
[Install]
WantedBy=multi-user.target
Важно не забыть про graceful shutdown: Node.js‑воркер должен уметь корректно завершать обработку текущих задач при получении SIGTERM, который будет посылать systemd. То же относится к PHP‑воркерам.

Тонкости cgroup v2 при пиковой нагрузке
Основная ошибка при работе с cgroup v2 — выставлять только MemoryMax, не думая про MemoryHigh и swap. Пара практических советов:
MemoryMaxзадавайте немного выше обычного потребления, а не «впритык»;MemoryHighможно использовать как «мягкий» предел — ядро начнёт активнее вытеснять страницы, но не будет моментально убивать процессы;CPUQuotaзадавайте с запасом относительно средней нагрузки, иначе под пиками увидите неравномерные задержки.
Если у вас есть критически важные сервисы (например, фронтовый API), им имеет смысл выделить больший CPUWeight или даже отдельный слайс, чтобы они не конкурировали с тяжёлыми воркерами на Node.js.
Логирование: journald против разрозненных логов
Смешанный стек PHP + Node.js часто порождает сборную солянку из логов: где‑то пишет в файлы, где‑то — в stdout, где‑то — в syslog. systemd упрощает эту картину:
- в юнитах выставляем
StandardOutput=journalиStandardError=journal; - в приложениях (PHP и Node.js) логируем в stdout или stderr с указанием уровня;
- просматриваем логи через
journalctl -u service-name.
Если затем вы захотите отправлять логи дальше (в ELK, Loki и т.п.), это делается уже на уровне journald или отдельного лог‑агента, без изменения приложений и юнитов.
Отказоустойчивость и watchdog systemd
Иногда нужно не только перезапускать упавший процесс, но и проверять его «здоровье» во время работы (например, воркер перестал принимать задачи, но процесс жив). Для таких случаев можно использовать WatchdogSec и протокол уведомлений systemd.
Идея в том, что приложение (PHP‑демон через CLI‑SAPI или Node.js‑процесс) периодически посылает в systemd «пинг» через sd_notify. Если пинг не приходит в течение заданного времени, systemd перезапускает сервис. Это более надёжно, чем просто надеяться на рестарт по коду выхода.
Для многих проектов достаточно базового Restart=on-failure, но под высокой нагрузкой и при сложной внутренней логике воркеров имеет смысл внедрить watchdog‑механику.
Типичные ошибки при запуске стека PHP + Node.js на одном VDS
Из практики администрирования проектов чаще всего встречаются такие проблемы:
- отсутствуют лимиты по памяти на Node.js и PHP‑воркерах — один memory leak кладёт весь VDS;
- всё крутится под одним пользователем и без sandbox‑директив systemd — повышает риски при компромате приложений;
- запуск через самописные bash‑скрипты в
rc.localили cron с @reboot — сложно дебажить и мониторить; - сервисы очередей (Redis, RabbitMQ) не имеют никаких ограничений и могут выжрать RAM под пиковыми нагрузками;
- воркеры не умеют корректно обрабатывать SIGTERM — при деплое ломаются задания в очереди.
Все эти проблемы решаются переходом на строгую схему: один демон — один systemd‑сервис с лимитами ресурсов и понятной политикой перезапуска плюс аккуратная настройка cgroup v2.
Резюме
На одном VDS вполне реально комфортно жить стеку «PHP + Node.js + Redis + RabbitMQ», если относиться к нему как к набору сервисов, а не как к одному монолитному серверу. systemd с поддержкой cgroup v2 позволяет:
- организовать запуск и остановку всех частей приложения предсказуемо;
- защитить систему от runaway‑процессов за счёт
MemoryMaxиCPUQuota; - отказаться от устаревших надстроек вроде supervisord в пользу нативных механизмов ОС;
- чётко разделить фоновые воркеры, веб‑фронтенды и сервисы очередей;
- проще дебажить за счёт единой точки логирования через journald.
Если вы только планируете запускать новый проект на VDS или реорганизуете существующий, начните с описания того, какие процессы у вас будут жить как отдельные systemd‑сервисы, какие лимиты и зависимости им нужны. После этого масштабирование (горизонтальное и вертикальное) становится вопросом инфраструктуры, а не ручной борьбы с демонами на каждом сервере. При необходимости всегда можно разделить роли по нескольким серверам или мигрировать часть нагрузки на виртуальный хостинг для менее критичных проектов.


