Написание unit-файла сервиса
Unit-файл сервиса systemd состоит из трёх секций: [Unit] для метаданных и зависимостей, [Service] для настройки процесса, [Install] для подключения к загрузке. ExecStart, Type, Restart, User и WorkingDirectory — пять параметров, которые ты будешь задавать в каждом сервисе.
У тебя есть Python API-сервер, который прекрасно работает при ручном запуске. Но когда ты уходишь — он останавливается. Когда сервер перезагружается — не поднимается. Когда падает — никто не замечает час. Все три проблемы решаются одним способом: unit-файл сервиса. Двенадцать строк INI-синтаксиса — и твой процесс становится полноценным гражданином systemd: контролируется, перезапускается, логируется и стартует при загрузке. Unit-файл — это не шаблон, который ты копируешь со Stack Overflow и забываешь. Каждый ключ — осознанный выбор, определяющий поведение процесса при падении в 3 часа ночи.
После этого урока ты сможешь написать минимальный, но готовый к production unit-файл, объяснить что означают Type=simple, Type=forking и Type=notify и когда использовать каждый, настроить автоматические перезапуски и запуск от непривилегированного пользователя, разместить файл в правильном месте и перезагрузить systemd после внесения изменений.
У 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 должен подтягивать этот сервис при включении.
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.
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 предотвращает бесконечные перезапуски при жёсткой ошибке конфигурации.
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После написания или редактирования 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.