OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2

Разбираем, как на одном VDS безопасно собрать PHP-бэкенд, Node.js-воркеры и WebSocket-сервисы, очереди на RabbitMQ или Redis, настроить systemd и cgroup v2, отказаться от supervisord и не получить хаос из процессов, перезапусков и неуправляемых логов.
PHP и Node.js на одном VDS: аккуратный запуск под systemd и cgroup v2

В типичном продакшн‑проекте сегодня легко оказаться в ситуации «всё сразу на одном 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 лучше опираться на нативные механизмы ОС. Это снижает количество подвижных частей и упрощает поддержку конфигурации.

Администратор планирует структуру сервисов PHP и Node.js и ресурсы 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 — его можно использовать как дополнение к этой статье.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Короткий обзор 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.

Виртуальный хостинг FastFox
Виртуальный хостинг для сайтов
Универсальное решение для создания и размещения сайтов любой сложности в Интернете от 95₽ / мес

Группировка сервисов через 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. Миграция обычно проходит по шаблону:

  1. Снять текущее описание процессов из supervisord.conf: что запускается, с какими аргументами.
  2. Для каждого процесса создать отдельный unit‑файл в /etc/systemd/system.
  3. Выставить аналогичные рестарт‑политики и рабочую директорию.
  4. Добавить лимиты по памяти и CPU (то, чего в supervisord часто не было вовсе).
  5. Постепенно отключать 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‑воркерам.

Дашборд мониторинга с лимитами CPU и памяти для сервисов systemd

Тонкости 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‑сервисы, какие лимиты и зависимости им нужны. После этого масштабирование (горизонтальное и вертикальное) становится вопросом инфраструктуры, а не ручной борьбы с демонами на каждом сервере. При необходимости всегда можно разделить роли по нескольким серверам или мигрировать часть нагрузки на виртуальный хостинг для менее критичных проектов.

Поделиться статьей

Вам будет интересно

Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик OpenAI Статья написана AI (GPT 5)

Docker Registry proxy cache на VDS: ускоряем CI и экономим трафик

Разбираем практическую схему: свой docker registry proxy cache на отдельном VDS, который прозрачно проксирует Docker Hub и другие ...
Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader OpenAI Статья написана AI (GPT 5)

Composer на VDS: быстрый install через packagist mirror и shm-подход к autoloader

В реальных проектах Composer крутится в каждом CI job и на каждом деплое, а vendor раздувается до сотен мегабайт. На PHP VDS с SSD ...
TCP, HTTPS и API на VDS: как уменьшить latency и настроить sysctl OpenAI Статья написана AI (GPT 5)

TCP, HTTPS и API на VDS: как уменьшить latency и настроить sysctl

Разбираем, из чего складывается задержка API на VDS и как на неё влияют TCP, HTTPS и параметры ядра Linux через sysctl. Покажу, ка ...