Как работает systemd: структура, юниты и как писать собственные сервисы
systemd — это система инициализации и менеджер служб, с которой начинается загрузка почти любого современного Linux. Он запускает процессы, следит за их состоянием, перезапускает упавшие, собирает логи и разруливает зависимости между сервисами.
Разберём, как он устроен, какие бывают юниты, как написать собственную службу и что настроить, чтобы она вела себя предсказуемо в продакшене.
Что такое systemd и чем он заменил init
Раньше службами управлял SysVinit: набор shell-скриптов в /etc/init.d/, которые выполнялись строго по очереди. Скрипт сам отвечал за запуск демона, запись pid-файла и проверку, жив ли процесс, — и делал это в каждом пакете по-своему.
systemd заменил этот зоопарк декларативным описанием: вы говорите, что нужно запустить и при каких условиях, а остальное берёт на себя система. Что это дало на практике:
- Параллельный запуск служб вместо последовательного — загрузка стала быстрее.
- Настоящий контроль процессов через cgroups: systemd знает обо всех потомках службы и корректно завершает их, а не теряет «осиротевшие» процессы.
- Единое логирование через journald, без разнобоя лог-файлов.
- Автоперезапуск упавших служб без сторонних supervisord и monit.
- Активация по требованию — через сокеты, таймеры или появление файла.
Юниты и их типы
Всё, чем управляет systemd, описывается юнитами — конфигурационными файлами, тип которых определяется расширением.
| Тип | Расширение | Назначение |
|---|---|---|
| Service | .service | Службы и демоны |
| Socket | .socket | Активация по обращению к сокету |
| Timer | .timer | Запуск по расписанию, замена cron |
| Mount | .mount | Точки монтирования |
| Path | .path | Реакция на появление или изменение файла |
| Target | .target | Группа юнитов, аналог уровней загрузки |
Файлы лежат в трёх местах, и это важно: /lib/systemd/system/ — юниты из пакетов, /etc/systemd/system/ — ваши собственные и переопределения (приоритет выше), /run/systemd/system/ — временные, до перезагрузки. Свои службы всегда кладите в /etc/systemd/system/: файл в /lib перезапишется при обновлении пакета.
Структура .service файла
Минимальный юнит для приложения на Node.js:
[Unit]
Description=My Node.js App
After=network-online.target postgresql.service
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/node /var/www/myapp/server.js
WorkingDirectory=/var/www/myapp
User=www-data
Environment=NODE_ENV=production
EnvironmentFile=-/etc/myapp/env
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetСекции делят описание по смыслу: [Unit] — метаданные и зависимости, [Service] — как запускать процесс, [Install] — что делать при systemctl enable.
Дефис в EnvironmentFile=-/etc/myapp/env означает «файла может не быть, это не ошибка». Без него служба не стартует, если файл отсутствует.
Type: чем отличаются simple, exec, forking и другие
Параметр Type= объясняет systemd, как понять, что служба запустилась. Ошибка здесь — самая частая причина странного поведения юнита.
simple(по умолчанию) — процесс, указанный вExecStart, и есть служба. systemd считает её запущенной сразу послеfork(), не дожидаясь готовности.exec— как simple, но статус «запущено» выставляется после успешногоexecve(). Ошибку вроде «нет такого файла» вы увидите сразу приsystemctl start, а не в логах.forking— для классических демонов, которые уходят в фон сами. ТребуетPIDFile=, иначе systemd потеряет процесс.oneshot— процесс отрабатывает и завершается; полезно для миграций и разовых задач, обычно вместе сRemainAfterExit=yes.notify— служба сама сообщает о готовности черезsd_notify(). Самый честный вариант: зависимые сервисы стартуют, только когда приложение действительно готово принимать запросы.
Для веб-приложения на simple есть подвох: зависимые юниты стартуют, когда процесс ещё инициализируется. Если это важно, используйте notify или проверку готовности на уровне приложения — как это устроено, разбирали в статье про health check сервиса.
Зависимости: After, Requires, Wants
Эти директивы часто путают, а отвечают они за разное.
After=— только порядок: наш юнит запустится после указанного. Не требует, чтобы тот вообще существовал.Requires=— жёсткая зависимость: если зависимость не поднялась, наш юнит не стартует. Порядок при этом не гарантируется, поэтомуRequires=почти всегда пишут вместе сAfter=.Wants=— мягкая: systemd попробует запустить зависимость, но её падение нашей службе не помешает. Разумный выбор по умолчанию.BindsTo=— жёстчеRequires=: если зависимость остановилась уже после запуска, наш юнит тоже будет остановлен.
Отдельная тонкость — network.target против network-online.target. Первый означает лишь, что сетевая подсистема настроена, а не что адрес получен. Приложениям, которым нужно сразу открывать соединения, требуется второй, причём вместе с Wants=network-online.target.
Перезапуск и устойчивость
Restart=always без ограничений — путь к бесконечному циклу перезапусков падающего приложения. Полезный набор параметров:
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
TimeoutStopSec=30Здесь служба перезапускается только при ненулевом коде выхода, с паузой в 5 секунд, но если за 5 минут случилось больше 5 попыток — systemd прекращает попытки и оставляет юнит в состоянии failed. Это лучше бесконечного цикла: сервис явно виден как сломанный, а не создаёт видимость работы.
TimeoutStopSec задаёт, сколько ждать корректного завершения перед SIGKILL. Если приложение умеет graceful shutdown, дайте ему на это время — иначе запросы будут обрываться при каждом деплое.
Изоляция и лимиты
systemd умеет ограничивать службу без контейнеров — несколько строк дают заметный выигрыш в безопасности:
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myappProtectSystem=strict монтирует всю файловую систему только на чтение, кроме явно разрешённых путей; PrivateTmp даёт службе собственный /tmp; NoNewPrivileges запрещает повышение привилегий. Проверить, насколько юнит защищён, можно командой systemd-analyze security myapp.service — она выставит оценку и подскажет, что ещё стоит включить.
Ограничения по ресурсам задаются там же: MemoryMax=512M, CPUQuota=50%. Под капотом это cgroups — подробности в статье про ограничение ресурсов через cgroups и systemd.
Запуск, изменение и диагностика
После создания или правки файла юнита обязательно перечитайте конфигурацию:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
systemctl status myapp.serviceНе редактируйте чужие юниты напрямую — используйте drop-in: systemctl edit nginx.service создаст файл в /etc/systemd/system/nginx.service.d/override.conf, где достаточно переопределить нужные параметры. Он переживёт обновление пакета.
Полезные команды при разборе проблем:
| Команда | Что показывает |
|---|---|
systemctl status myapp -l --no-pager | Состояние, последние строки лога, код выхода |
journalctl -u myapp -f | Логи службы в реальном времени |
journalctl -u myapp -b -p err | Только ошибки с момента загрузки |
systemctl list-units --failed | Все упавшие юниты |
systemctl cat myapp | Итоговый юнит со всеми drop-in |
systemd-analyze blame | Что дольше всего стартовало при загрузке |
systemd-analyze verify myapp.service | Проверка синтаксиса до запуска |
Тонкости фильтрации логов journald — в отдельном материале про анализ логов с journalctl.
Таймеры вместо cron
Периодические задачи в systemd описываются парой юнитов: .service с самой работой и .timer с расписанием. В отличие от cron, таймеры логируются в journald, умеют догонять пропущенные запуски (Persistent=true) и наследуют все настройки изоляции и лимитов. Разбор с примерами — в статье про systemd timers как замену cron.
Заодно совет по надёжности: если задача критична, факт её выполнения стоит контролировать снаружи — heartbeat-мониторинг заметит молчание таймера, даже если сервер жив и никаких алертов не прислал.
FAQ
Почему служба не стартует, а status показывает code=exited, status=203/EXEC?
systemd не смог запустить исполняемый файл: неверный путь в ExecStart, нет прав на выполнение или указан не абсолютный путь. Относительные пути в юнитах не работают.
Почему переменные из .bashrc не видны в службе?
Служба не запускает интерактивный shell. Переменные задаются через Environment= или EnvironmentFile=.
Чем enable отличается от start?
start запускает службу сейчас, enable включает автозапуск при загрузке. enable --now делает и то, и другое.
Как посмотреть, что именно сейчас применяется к юниту?
systemctl cat myapp покажет итоговую конфигурацию со всеми переопределениями, а systemctl show myapp — все параметры с фактическими значениями.
Похожие статьи

NUMA-aware приложения — как писать софт с учётом NUMA
Разбираем архитектуру NUMA, проблемы удалённой памяти и практические подходы к разработке NUMA-aware приложений.
4 мая 20266 мин

Backpressure: как не положить сервис под нагрузкой
Разбираем механизм backpressure в распределённых системах: почему сервисы падают под нагрузкой, как управлять скоростью обработки запросов и какие паттерны помогают избежать перегрузки.
10 марта 20268 мин

Лучшие сервисы мониторинга сайтов: сравнение и как выбрать
Критерии выбора, сравнительная таблица и разбор сервисов: облачных, российских и self-hosted — с ценами на август 2026.
24 марта 20258 мин
Настроить мониторинг за 30 секунд
Надежные оповещения о даунтаймах. Без ложных срабатываний