В продакшне почти любое веб‑приложение на 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 становится предсказуемой.

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. Это критично для аккуратного завершения обработки задач очереди.
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.

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‑файлов.
Общие приёмы и полезные опции для 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‑воркеров логика обычно следующая:
- Останавливаем воркеры:
systemctl stop php-worker@*.service. - Обновляем код (git pull, rsync и т. д.).
- Обновляем зависимости:
composer install. - Запускаем воркеры:
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‑юнитом:
- Скачиваем новый образ:
docker pull myregistry/myapp:latest. - Перезапускаем сервис:
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 или на надёжном виртуальном хостинге делом пары минут.


