Выберите продукт

PHP, Node.js и Docker под systemd: готовые шаблоны для VDS

Практическое руководство по запуску PHP-демонов, Node.js-приложений и Docker-контейнеров через systemd на VDS. Разберём шаблоны unit-файлов, автозапуск, перезапуск, логирование и лимиты ресурсов. Примеры помогут сервисам стабильно переживать рестарты и обновления ядра.
PHP, Node.js и Docker под systemd: готовые шаблоны для VDS

В продакшне почти любое веб‑приложение на VDS рано или поздно упирается в один и тот же вопрос: как правильно запускать процессы. Сервис на PHP, фоновые воркеры на Node.js, несколько Docker‑контейнеров — всё это надо надёжно стартовать при загрузке, рестартовать при падениях и аккуратно гасить при деплоях.

На Linux‑сервере стандартный ответ — systemd. Но у него своя терминология (unit, service, target, timer), много опций и нюансов, а документация довольно сухая. В этой статье я собрал практичный набор шаблонов unit‑файлов для PHP, Node.js и Docker, которые подойдут для большинства типичных сценариев на VDS.

Подход: почему именно systemd, а не screen, nohup или pm2

Старые привычки часто тянут в сторону screen/tmux, nohup ... & или обёрток вроде pm2. Для разработки это терпимо, но в продакшне у такого подхода есть минусы:

  • Нет нормальной интеграции с перезапуском сервера (boot‑order, зависимости).
  • Сложнее централизованно смотреть логи и статусы.
  • Нет строгих лимитов по памяти/CPU и понятного управления перезапусками.

systemd же даёт:

  • Единый стандарт управления сервисами: systemctl start/stop/status.
  • Логи через journalctl, без отдельной настройки лог‑файлов.
  • Лимиты ресурсов (через cgroup), рестарты, зависимости от сети/БД и т. д.
  • Удобные шаблоны unit‑файлов (инстанс‑сервисы), которые мы и будем использовать.

И при этом никто не запрещает комбинировать: например, Node.js‑приложение может запускаться через pm2, но сам pm2 контролировать через systemd. Однако в большинстве случаев проще сразу завернуть в systemd само приложение.

Базовый каркас systemd unit‑файла

Прежде чем переходить к PHP, Node.js и Docker, зафиксируем общую структуру .service:

[Unit]
Description=My App
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/myapp --flag
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Кратко по ключевым полям, которые будем использовать во всех шаблонах:

  • Description — короткое описание, видно в systemctl status.
  • After — порядок старта. Для веб‑приложений почти всегда достаточно After=network.target.
  • User/Group — под каким пользователем крутится сервис. Не запускайте всё под root.
  • WorkingDirectory — корень проекта, откуда будет запущен процесс.
  • ExecStart — собственно команда запуска.
  • Restart и RestartSec — политика перезапуска.
  • WantedBy — к какому «таргету» привязываем автозапуск. Для серверов обычно multi-user.target.

Файлы юнитов для собственных сервисов обычно кладём в /etc/systemd/system/, затем выполняем:

systemctl daemon-reload
systemctl enable myapp.service
systemctl start myapp.service

Если вы поднимаете несколько проектов на одном сервере (PHP+Node.js+Docker), удобно сразу выработать единый стиль имён: php-worker@queue.service, node-app@api.service, docker-app@worker.service — так навигация по systemctl и journalctl становится предсказуемой.

Экран сервера с unit-файлами systemd и логами сервисов

PHP как сервис: демоны, воркеры, очереди

Классический PHP (через FPM) уже обычно управляется отдельным systemd‑юнитом. Но всё чаще в проектах появляются длительно живущие PHP‑процессы:

  • воркеры очередей (Symfony Messenger, Laravel Queue, собственные демоны),
  • планировщики задач (аналог крон‑воркера),
  • простые TCP/HTTP‑демоны на чистом PHP.

Запускать их через screen или простой php worker.php & неудобно — сервисы не поднимаются после ребута, а падения незаметны.

Шаблон systemd для PHP‑воркера

Простейший шаблон для PHP‑скрипта, работающего как демон (бесконечный цикл, обработка очереди и т. д.):

[Unit]
Description=PHP Worker: myapp queue
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/php artisan queue:work --sleep=3 --tries=3
Restart=always
RestartSec=5

# Безопасные дефолты (можно поджать под конкретный VDS)
MemoryMax=512M
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Что здесь важно:

  • Type=simple — стандартный случай: запускаем процесс и считаем сервис запущенным.
  • Restart=always — перезапускать даже после ручного выхода с кодом 0. Для воркеров очередей обычно это удобно.
  • MemoryMax — не даст процессу съесть всю память VDS, сервис будет перезапущен при выходе за лимит.

Логи такого воркера по умолчанию попадают в journal:

journalctl -u php-worker-myapp.service -f

Инстанс‑сервис для множества PHP‑воркеров

Часто нужно несколько одинаковых воркеров с разными параметрами: например, очередь high, default, low. Для этого удобно использовать шаблонный сервис с @:

# /etc/systemd/system/php-worker@.service
[Unit]
Description=PHP Worker instance %i
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp
ExecStart=/usr/bin/php artisan queue:work --queue=%i --sleep=3 --tries=3
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

Запуск инстансов:

systemctl daemon-reload
systemctl enable php-worker@high.service
systemctl enable php-worker@default.service
systemctl enable php-worker@low.service

systemctl start php-worker@high.service
systemctl start php-worker@default.service
systemctl start php-worker@low.service

Здесь %i подставляется из имени инстанса (часть между @ и .service). Такой приём отлично масштабируется: можно поднять, например, php-worker@payment.service, php-worker@emails.service и т. д. без дублирования кода юнита.

Грейсфул‑рестарт PHP‑воркера

Если ваш PHP‑демон умеет ловить SIGTERM и корректно завершаться, systemd сам отправит сигналы при stop/restart. Но иногда нужно настроить таймаут:

[Service]
# ... остальное
TimeoutStopSec=30
KillSignal=SIGTERM
KillMode=mixed

Так systemd сперва отправит SIGTERM и подождёт до 30 секунд, прежде чем убить процесс SIGKILL. Это критично для аккуратного завершения обработки задач очереди.

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

Node.js под systemd: простой сервис и шаблоны

Для Node.js многие привыкли использовать pm2, но для небольших и средних проектов на VDS надёжнее (и проще) положиться на systemd напрямую. Ниже — универсальный шаблон для Node.js‑приложения, которое слушает порт (HTTP API, веб‑сервер, WebSocket‑бекенд и т. п.).

Базовый unit‑файл для Node.js приложения

# /etc/systemd/system/node-app.service
[Unit]
Description=Node.js API server
After=network.target

[Service]
Type=simple
User=node
Group=node
WorkingDirectory=/srv/nodeapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5

# Ресурсные лимиты, адаптируйте под свой VDS
MemoryMax=512M
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

Рекомендации:

  • Создайте отдельного пользователя node, не гоняйте сервис под root.
  • Используйте Environment= вместо .env для чувствительных параметров, либо EnvironmentFile= с доступом 600.
  • Restart=on-failure — не перезапускать после штатного завершения (код 0), но рестартовать при падении.

Если вы строите API‑слой поверх PHP‑монолита, имеет смысл продумать схему портов и firewall. Мы разбирали сетевые нюансы для контейнеров и приложений в материале про настройку firewall для Docker и сервисов на VDS: iptables и nftables для Docker и приложений на VDS.

Инстанс‑сервисы для нескольких Node.js приложений

Если на одном VDS вы хостите несколько небольших Node.js‑сервисов, удобно использовать шаблон:

# /etc/systemd/system/node-app@.service
[Unit]
Description=Node.js app instance %i
After=network.target

[Service]
Type=simple
User=node
Group=node
WorkingDirectory=/srv/nodeapps/%i
Environment=NODE_ENV=production
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Создаём директории /srv/nodeapps/app1, /srv/nodeapps/app2 и запускаем:

systemctl daemon-reload
systemctl enable node-app@app1.service
systemctl enable node-app@app2.service

systemctl start node-app@app1.service
systemctl start node-app@app2.service

Так вы получаете единый шаблон обслуживания множества проектов, каждый живёт в своей директории и имеет собственный статус/логи:

journalctl -u node-app@app1.service -f

Грейсфул‑рестарт Node.js сервера

Node.js по умолчанию завершает процесс при SIGTERM. Главное — в своём коде корректно закрывать HTTP‑сервер и активные соединения, чтобы избежать обрыва запросов. Примерный паттерн внутри приложения:

const server = app.listen(process.env.PORT || 3000);

process.on('SIGTERM', () => {
  server.close(() => {
    process.exit(0);
  });
});

Со стороны systemd зачастую достаточно базовых настроек. Если приложение иногда «зависает» при остановке, можно добавить TimeoutStopSec и KillMode, аналогично примерам для PHP.

Терминал Linux с командами systemctl и docker для управления контейнерами

Docker + systemd: когда и как это имеет смысл

Если у вас несколько контейнеров, которыми управляет docker-compose, то проще всего запускать сам docker-compose или аналог через один unit. Но иногда нужен полный контроль над отдельным контейнером как системным сервисом — с рестартом, зависимостями и unit‑шаблонами.

Важно: речь не о запуске самого Docker‑демона (обычно он уже управляется стандартным пакетом), а о пользовательских контейнерах.

Один контейнер как systemd‑сервис

Пример — Node.js сервис упакован в образ myapp:latest и должен запускаться с определёнными переменными окружения и томами.

# /etc/systemd/system/docker-myapp.service
[Unit]
Description=Docker container: myapp
Requires=docker.service
After=docker.service

[Service]
Type=simple
Restart=always
RestartSec=5

# Останавливаем старый контейнер перед запуском
ExecStartPre=/usr/bin/docker rm -f myapp
ExecStart=/usr/bin/docker run --name myapp -p 3000:3000 -v /srv/myapp/config:/app/config -e NODE_ENV=production myregistry/myapp:latest
ExecStop=/usr/bin/docker stop myapp

[Install]
WantedBy=multi-user.target

Здесь команда запуска записана одной строкой, как и требует systemd. Если параметров становится много, вынесите их в отдельный скрипт и вызывайте его из ExecStart.

Ключевые моменты:

  • Requires=docker.service и After=docker.service — гарантируем, что Docker‑демон уже запущен.
  • ExecStartPre — удаляем старый контейнер, чтобы не получить конфликт по имени.
  • Restart=always — перезапуск контейнера при любом выходе.

Шаблонный unit для множества контейнеров

Если у вас несколько похожих контейнеров (несколько инстансов одного приложения или разные мелкие сервисы), удобно использовать шаблон с @:

# /etc/systemd/system/docker-app@.service
[Unit]
Description=Docker container instance %i
Requires=docker.service
After=docker.service

[Service]
Type=simple
Restart=always
RestartSec=5

ExecStartPre=/usr/bin/docker rm -f app-%i
ExecStart=/usr/bin/docker run --name app-%i myregistry/app-%i:latest
ExecStop=/usr/bin/docker stop app-%i

[Install]
WantedBy=multi-user.target

Тогда запуск нескольких контейнеров выглядит так:

systemctl daemon-reload
systemctl enable docker-app@api.service
systemctl enable docker-app@worker.service

systemctl start docker-app@api.service
systemctl start docker-app@worker.service

Это удобно, когда у вас набор похожих микросервисов и вы хотите управлять ими одинаково, без копипасты unit‑файлов.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Общие приёмы и полезные опции для PHP, Node.js и Docker

Теперь соберём воедино типичные опции systemd, которые полезны для всех трёх случаев.

Работа с окружением и секретами

Вместо жёстко зашитых переменных окружения прямо в ExecStart лучше использовать Environment и EnvironmentFile:

[Service]
Environment=NODE_ENV=production
Environment=APP_ENV=prod
EnvironmentFile=/etc/myapp/myapp.env
ExecStart=/usr/bin/php /srv/myapp/artisan queue:work

Файл /etc/myapp/myapp.env может выглядеть так:

APP_URL=https://example.com
DB_HOST=127.0.0.1
DB_PASSWORD=secret

Не забывайте выставить права 600 и владельца, под которым работает сервис, чтобы пароль от БД не лежал в мире‑читабельном конфиге.

Логи: journalctl и ротация

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

  • journalctl -u php-worker@payment.service -f — «хвост» логов;
  • journalctl -u node-app.service --since "2025-01-01" — отбор по дате;
  • journalctl -u docker-myapp.service -n 100 — последние 100 строк.

Размер /var/log/journal стоит ограничить через конфиг journald (например, SystemMaxUse), чтобы логи не съели весь диск на VDS. Если у вас много метрик и логов, посмотрите в сторону выноса сбора телеметрии на отдельный инстанс, как в статье про одиночный узел с VictoriaMetrics на VDS: развёртывание одиночного узла VictoriaMetrics на VDS.

Ресурсные лимиты для стабильного VDS

На небольших VDS особенно важно не допустить, чтобы один процесс «съел всё». Для этого можно добавить в [Service]:

MemoryMax=512M
CPUQuota=80%
LimitNOFILE=65535

Пара замечаний:

  • MemoryMax работает через cgroup: при превышении лимита процесс убьют, systemd его перезапустит (если так настроено).
  • CPUQuota ограничивает максимально доступный CPU‑процент (при 2 vCPU 200% — это оба ядра на 100%).
  • LimitNOFILE полезно поднять для Node.js и PHP‑воркеров с большим количеством параллельных соединений.

Жёсткая изоляция: что можно включать по умолчанию

Без фанатизма можно добавить базовые sandbox‑опции, чтобы усложнить жизнь потенциальному злоумышленнику в случае уязвимости в приложении:

[Service]
# ... остальное
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full

Но некоторые фреймворки и инструменты деплоя могут требовать доступ к домашнему каталогу, записи в системные директории и т. д. Поэтому включайте эти опции постепенно, тестируя окружение на стенде перед продакшном.

Жизненный цикл: деплой, обновления, перезапуски

Чтобы интеграция с systemd не мешала деплою (а помогала), стоит отработать типовой сценарий обновления.

PHP: обновление кода и рестарт воркеров

Для PHP‑воркеров логика обычно следующая:

  1. Останавливаем воркеры: systemctl stop php-worker@*.service.
  2. Обновляем код (git pull, rsync и т. д.).
  3. Обновляем зависимости: composer install.
  4. Запускаем воркеры: systemctl start php-worker@*.service.

При наличии нескольких серверов или большого объёма очереди имеет смысл реализовать внутри самого воркера обработку SIGTERM: завершать текущую задачу и потом только выходить.

Node.js: zero‑downtime перезапуск с systemd

Для минимизации даунтайма можно использовать rolling‑деплой с двумя инстансами и балансировщиком (nginx/haproxy), но если у вас один сервер и одно приложение, простой рестарт часто приемлем:

systemctl restart node-app.service

Если в коде реализован корректный обработчик SIGTERM (грейсфул‑шутдаун), то активные запросы завершатся, новые подключения перестанут приниматься, и только после этого процесс выйдет. Nginx сверху при этом сможет корректно отыграть 502 при краткой недоступности, если вы настроили повторные попытки (retry) и max_fails.

Docker: обновление образа с минимальным даунтаймом

Типичный сценарий для контейнера, управляемого systemd‑юнитом:

  1. Скачиваем новый образ: docker pull myregistry/myapp:latest.
  2. Перезапускаем сервис: systemctl restart docker-myapp.service.

В шаблоне с ExecStartPre=/usr/bin/docker rm -f myapp старый контейнер будет удалён, новый запущен с теми же параметрами. Если вам нужен более сложный zero‑downtime деплой (blue‑green), его лучше реализовать на уровне оркестрации или через внешний балансировщик.

Отладка проблем с systemd‑сервисами

Даже при аккуратной настройке PHP, Node.js и Docker под systemd иногда что‑то идёт не так. Краткий чек‑лист диагностики:

  • systemctl status myservice.service — первое место, куда смотреть. Обратите внимание на Active и последние строки логов.
  • journalctl -u myservice.service -b — логи с момента последней загрузки.
  • Проверьте права на директории WorkingDirectory и файлы конфигов: пользователь сервиса должен иметь доступ.
  • Проверьте параметры ExecStart вручную в шелле (под нужным пользователем) — многие проблемы проявляются ещё до systemd.

Если сервис не стартует и в выводе systemctl status мало информации, почти всегда стоит запустить ту же самую команду вручную и посмотреть stdout и stderr напрямую.

Выводы

Использование systemd как единой точки управления PHP‑воркерами, Node.js‑приложениями и Docker‑контейнерами сильно упрощает жизнь админа и девопса на VDS. Шаблонные unit‑файлы с инстансами через @ позволяют обслуживать десятки сервисов без копипасты, а встроенные механизмы рестартов, логирования и лимитов помогают сделать инфраструктуру предсказуемой и устойчивой.

В качестве отправной точки берите базовые шаблоны из этой статьи, адаптируя пути, пользователей и лимиты под ваши проекты. Со временем вы сформируете собственную библиотеку unit‑файлов, которая сделает развёртывание новых PHP, Node.js и Docker‑сервисов на VDS или на надёжном виртуальном хостинге делом пары минут.

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

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

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...