open atlas
↑ К треку
Linux: операционная система LIN · 03 · 02

Написание unit-файла сервиса

Unit-файл сервиса systemd состоит из трёх секций: [Unit] для метаданных и зависимостей, [Service] для настройки процесса, [Install] для подключения к загрузке. ExecStart, Type, Restart, User и WorkingDirectory — пять параметров, которые ты будешь задавать в каждом сервисе.

LIN Middle ◷ 22 min
Уровень
ОсновыJuniorMiddleSenior

У тебя есть Python API-сервер, который прекрасно работает при ручном запуске. Но когда ты уходишь — он останавливается. Когда сервер перезагружается — не поднимается. Когда падает — никто не замечает час. Все три проблемы решаются одним способом: unit-файл сервиса. Двенадцать строк INI-синтаксиса — и твой процесс становится полноценным гражданином systemd: контролируется, перезапускается, логируется и стартует при загрузке. Unit-файл — это не шаблон, который ты копируешь со Stack Overflow и забываешь. Каждый ключ — осознанный выбор, определяющий поведение процесса при падении в 3 часа ночи.

Цель

После этого урока ты сможешь написать минимальный, но готовый к production unit-файл, объяснить что означают Type=simple, Type=forking и Type=notify и когда использовать каждый, настроить автоматические перезапуски и запуск от непривилегированного пользователя, разместить файл в правильном месте и перезагрузить systemd после внесения изменений.

1

У unit-файла три секции. Знание того, какой ключ в какой секции, устраняет большую часть путаницы. Помести файл в /etc/systemd/system/myapp.service:

[Unit]
Description=My Python API
After=network.target

[Service]
ExecStart=/usr/bin/python3 /opt/myapp/server.py
Restart=on-failure
User=myapp
WorkingDirectory=/opt/myapp

[Install]
WantedBy=multi-user.target

[Unit] содержит метаданные и объявления зависимостей — всё, что верно независимо от типа юнита. [Service] содержит конфигурацию процесса: как запускать, от какого пользователя, поведение при перезапуске. [Install] содержит подключение для systemctl enable — какой target должен подтягивать этот сервис при включении.

2

Type= говорит systemd, как твой процесс сигнализирует о готовности. Ошибка здесь вызывает скрытые баги — systemd может объявить сервис “active” до того, как он реально обслуживает запросы, или зависнуть в ожидании готовности.

TypeКогда systemd считает сервис “запущенным”Когда использовать
simple (по умолчанию)Сразу после fork(), до каких-либо действий процессаПроцесс остаётся на переднем плане; большинство современных демонов
forkingПосле завершения родительского процессаПроцесс уходит в фон через двойной fork (старые демоны, например legacy nginx)
notifyКогда процесс отправляет sd_notify(READY=1)Процесс явно сигнализирует готовность (нативные демоны systemd)
oneshotПосле завершения ExecStart (для скриптов)Миграционные задачи, однократная настройка
# Современный foreground-демон (Node.js, Python uvicorn, Go http.ListenAndServe)
[Service]
Type=simple
ExecStart=/usr/bin/node /opt/app/index.js

# Legacy nginx с двойным fork
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStart=/usr/sbin/nginx

Если задать Type=forking для процесса, который не делает fork, systemd будет ждать завершения родителя — которое никогда не наступит — и через 90 секунд пометит сервис как failed.

3

Restart= управляет тем, что происходит при неожиданном завершении процесса. Это одна из важнейших директив — разница между “разбудили в 3 ночи” и “сервис восстановился сам”:

ЗначениеПерезапускает приКогда использовать
no (по умолчанию)НикогдаОднократные задачи; ручное управление
on-failureНенулевой код выхода или сигналProduction-сервисы — перезапуск при сбое, но не при чистой остановке
alwaysЛюбое завершение включая exit 0Постоянные сервисы, которые всегда должны работать
on-abnormalСигнал или таймаут watchdog (не чистый выход)Похоже на on-failure, но исключает ненулевой код выхода
[Service]
Restart=on-failure
RestartSec=5s          # Ждать 5 секунд перед перезапуском (избегает тесных петель)
StartLimitIntervalSec=60s  # Окно для подсчёта попыток перезапуска
StartLimitBurst=3      # Если перезапустился 3 раза за 60с — сдаться и войти в failed

Без RestartSec падающий сервис перезапускается немедленно и может спамить логи или нагружать CPU. Пара StartLimitBurst / StartLimitIntervalSec предотвращает бесконечные перезапуски при жёсткой ошибке конфигурации.

4

User=, Group= и WorkingDirectory= обязательны для production-сервисов. Запуск от root — угроза безопасности: скомпрометированный процесс получает все ключи от замка:

[Service]
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
RuntimeDirectory=myapp        # Создаёт /run/myapp с владельцем User= при старте
StateDirectory=myapp          # Создаёт /var/lib/myapp с владельцем User= при старте
LogsDirectory=myapp           # Создаёт /var/log/myapp с владельцем User= при старте

RuntimeDirectory, StateDirectory и LogsDirectory — чистый способ systemd создавать и владеть директориями при старте сервиса. Никаких хаков с ExecStartPre=mkdir, директории создаются с правильными правами автоматически.

Пользователь myapp должен быть системным пользователем, созданным при деплое:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
5

После написания или редактирования unit-файла нужно сказать systemd перечитать его через daemon-reload. systemd кешируем unit-файлы. Без daemon-reload он продолжит использовать старое определение, даже если перезапустить сервис:

# После создания или редактирования /etc/systemd/system/myapp.service
sudo systemctl daemon-reload

# Теперь новое определение активно — запустить или перезапустить
sudo systemctl enable --now myapp.service

# Убедиться, что работает как ожидается
systemctl status myapp.service

# Наблюдать живые логи для этого сервиса
journalctl -u myapp.service -f

Самый частый вопрос в поддержке: “Я обновил unit-файл, но ничего не изменилось.” В девяти случаях из десяти был пропущен daemon-reload.

Разбор примера

Превратить скрипт, запускаемый вручную, в контролируемый сервис.

У команды есть фоновая задача worker.py, которую они запускают в tmux-сессии. Нужно, чтобы она переживала перезагрузки и перезапускалась при падении.

# /etc/systemd/system/worker.service
[Unit]
Description=Background task worker
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=worker
Group=worker
WorkingDirectory=/opt/worker
ExecStart=/opt/worker/venv/bin/python worker.py
Environment=DATABASE_URL=postgresql://localhost/mydb
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=120s
StartLimitBurst=5
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Деплой и запуск:

sudo systemctl daemon-reload
sudo systemctl enable --now worker.service
systemctl status worker.service
journalctl -u worker.service --since "5 minutes ago"

Объяснение ключевых выборов: After=postgresql.service гарантирует, что postgres запущен до попытки подключения worker. Environment= инжектирует конфиг без .env-файла. StandardOutput=journal направляет stdout в журнал — journalctl -u worker показывает всё.

Частая ошибка

Не помещай секреты в строку Environment= unit-файла — эти значения видны в выводе systemctl show и в журнале, а сам файл на большинстве дистрибутивов доступен для чтения всем. Вместо этого используй EnvironmentFile=/etc/worker.env с chmod 600 и владельцем-пользователем сервиса. Ещё лучше на современных Ubuntu/Debian: используй LoadCredential= (systemd v250+) для инжекции секретов через credential store.

Почему это работает

Разделение /etc/systemd/system/ и /lib/systemd/system/ существует потому, что /lib/ принадлежит пакетным менеджерам. Когда запускается apt upgrade nginx, он перезаписывает /lib/systemd/system/nginx.service версией из пакета. Файлы в /etc/systemd/system/ пакетный менеджер никогда не трогает. Можно также использовать drop-in переопределения — создай /etc/systemd/system/nginx.service.d/override.conf и помести туда только изменяемые ключи. Drop-in сливается во время выполнения; ты переопределяешь конкретную директиву без копирования всего unit-файла.

Проверь себя
Викторина

Ты редактируешь /etc/systemd/system/myapp.service, добавляя Restart=on-failure, затем запускаешь `sudo systemctl restart myapp`. Позже обнаруживаешь, что сервис по-прежнему не перезапускается при падении. Что ты скорее всего забыл?

Итог

Unit-файл сервиса systemd имеет три обязательные секции: [Unit] для метаданных и объявления зависимостей (Description, After, Requires), [Service] для конфигурации процесса (ExecStart, Type, Restart, User, WorkingDirectory) и [Install] для подключения к загрузке (WantedBy). Type=simple подходит для foreground-демонов; Type=forking — для процессов с двойным fork; Type=notify — для нативных демонов systemd. Restart=on-failure с RestartSec предотвращает превращение crash-петель в простой. Всегда запускай daemon-reload после редактирования unit-файла — пропуск этого шага — причина номер один “моё изменение ничего не сделало”. Помещай локальные unit-файлы в /etc/systemd/system/, никогда в /lib/systemd/system/.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.