Короткий ответ: свой сервис в systemd — это unit-файл в /etc/systemd/system/ с тремя секциями: [Unit] описывает зависимости, [Service] — что и от чьего имени запускать, [Install] — когда включать. После создания файла выполняются systemctl daemon-reload и systemctl enable --now имя. Дальше приложение запускается при загрузке, перезапускается при падении, а его вывод попадает в journald.
Для сайтов и ботов это описано в услуге разработки и поддержки сайтов; здесь — техническая часть.
Зачем systemd вместо nohup и screen
| Подход | Переживёт перезагрузку | Перезапуск при падении | Логи |
|---|---|---|---|
nohup node app.js & |
Нет | Нет | Файл, который надо чистить |
screen / tmux |
Нет | Нет | Внутри сессии |
| systemd | Да | Да, настраивается | journald с ротацией |
| Docker + restart-политика | Да | Да | docker logs |
systemd уже есть в системе, не требует установки и управляет порядком запуска: база данных поднимется раньше приложения, если это указать. Для процессов, которые не являются контейнерами, это самый простой способ сделать их постоянными.
Минимальный unit-файл
Создайте /etc/systemd/system/myapp.service:
[Unit]
Description=My Application
Documentation=https://example.com/docs
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
Group=app
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node /opt/myapp/index.js
Restart=on-failure
RestartSec=5
Environment=NODE_ENV=production
Environment=PORT=3000
[Install]
WantedBy=multi-user.target
Разбор ключевых строк:
| Директива | Что делает |
|---|---|
After=network-online.target |
Ждать, пока сеть действительно поднимется, а не просто инициализируется |
Type=simple |
Основной процесс не форкается — обычный случай для Node.js, Python, Go |
User / Group |
Процесс работает не от root. Отдельный пользователь обязателен |
WorkingDirectory |
Рабочий каталог; относительные пути считаются от него |
ExecStart |
Абсолютный путь к бинарнику и абсолютный путь к скрипту |
Restart=on-failure |
Перезапуск при ненулевом коде выхода или сигнале |
RestartSec=5 |
Пауза перед перезапуском, чтобы не устроить цикл |
Путь к интерпретатору нужно указывать полностью. Если Node.js поставлен через mise, which node покажет путь внутри домашнего каталога версии — его и надо использовать, иначе systemd не найдёт бинарник, потому что переменные окружения пользователя ему недоступны.
Type=simple, forking и oneshot
| Type | Когда нужен |
|---|---|
simple |
Процесс не отвязывается от терминала — Node.js, Python, Go, большинство приложений |
forking |
Программа сама уходит в фон и создаёт дочерний процесс — старые демоны |
oneshot |
Задача выполняется один раз и завершается — скрипты, миграции |
notify |
Приложение само сообщает о готовности через sd_notify |
Ошибка в Type — частая причина того, что сервис «запускается и сразу останавливается». Если приложение не форкается, а указан forking, systemd будет считать, что процесс завершился.
Управление
# перечитать unit-файлы после правки
systemctl daemon-reload
# включить автозапуск и запустить сейчас
systemctl enable --now myapp
# состояние и последние строки лога
systemctl status myapp
# логи в реальном времени
journalctl -u myapp -f
# логи за сегодня
journalctl -u myapp --since today
# только ошибки
journalctl -u myapp -p err
# перезапуск
systemctl restart myapp
# остановить и убрать из автозапуска
systemctl disable --now myapp
daemon-reload обязателен после каждой правки unit-файла. Без него systemd продолжит использовать старую версию, и изменения не применятся.
Переменные окружения и секреты
Секреты не стоит писать в Environment= — unit-файл читается всеми. Правильнее вынести их в отдельный файл:
# /etc/myapp/env — права 600, владелец root
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app:пароль@127.0.0.1:5432/app
И подключить в секции [Service]:
EnvironmentFile=/etc/myapp/env
Права на файл: chmod 600 /etc/myapp/env. Приложение читает переменные так же, как если бы они были заданы в unit-файле.
Ограничения ресурсов
В [Service] можно задать потолки — полезно, чтобы одно приложение не выело всю память сервера:
MemoryMax=1G
MemoryHigh=800M
CPUQuota=150%
TasksMax=256
LimitNOFILE=65535
При превышении MemoryMax процесс будет убит, и Restart=on-failure поднимет его заново. Это лучше, чем отказ всего сервера: без ограничений утечка памяти в одном сервисе роняет базу данных, nginx и всё остальное. Общий разбор темы — в материале про резервное копирование и обслуживание VPS.
Типичные ошибки
- Относительный путь в
ExecStart. systemd не ищет бинарники вPATHпользователя — только абсолютные пути. - Забыт
daemon-reload. Правки не применяются, и это выглядит как «сервис игнорирует настройки». - Работа от root. Не нужно и опасно: при компрометации приложения злоумышленник получает всю машину.
- Нет
Restart. После первой же ошибки сервис останется лежать до ручного вмешательства. - Секреты в
Environment=. Видны всем, у кого есть доступ к unit-файлу, и попадают в выводsystemctl show. Type=forkingдля приложения, которое не форкается. systemd решит, что сервис завершился, и остановит его.
Проверка после настройки
systemctl is-enabled myapp— должен ответитьenabled.systemctl is-active myapp— должен ответитьactive.- Убить процесс вручную (
kill -9) и убедиться, что он поднялся заново. - Перезагрузить сервер и проверить, что сервис запустился сам.
- Посмотреть
journalctl -u myapp -p err— ошибок быть не должно.
