systemd: как сервис живёт на сервере

Это вторая статья серии «Эксплуатация VPS / DevOps-база». В прошлый раз (Анатомия VPS: от голого Ubuntu до рабочего сервера) мы поставили фундамент: зашли по SSH по ключу, создали пользователя, закрыли root, включили swap. Сервер жив, но пока умеет только одно — пускать вас по SSH. Чтобы на нём появился сайт, должны жить и сами программы: веб-сервер nginx, обработчик PHP-FPM, база MariaDB, планировщик задач. Каждая из них должна запускаться сама после перезагрузки, уметь перезапускаться после падения и слушаться одной команды управления. За это на вашей машине уже отвечает systemd — и вы его уже ненароком тронули в прошлой статье командой systemctl restart ssh, даже не разобравшись что это.

Разберём systemd по-человечески. Без магии и без сухого списка команд: что вообще такое «сервис», из чего сделан файл, который говорит systemd как его вести, чем «запустить» отличается от «включить в автозагрузку» (это два разных переключателя!), как сервису сказать «дождись базы, потом стартуй», и главное — как читать его логи и понимать, почему он упал, когда в три ночи сайт лежит. На голом systemd-понимании держатся и nginx (следующая, часть III серии), и PHP-FPM (часть IV), и мониторинг (часть VIII), и аварийная диагностика (финальная часть IX). Прочитав эту статью, вы перестанете вводить systemctl как заклинание и начнёте понимать, что делает каждая буква.

Термины, которые введём один раз и дальше будем экономить: демон (daemon, читается «демон») — программа, которая живёт в фоне и что-то обслуживает без вашего прямого участия (nginx — демон, который ждёт и отдаёт запросы). Юнит (unit, «блок») — то, чем systemd управляет; у демонов тип юнита называется service, но бывают и другие (таймеры, соккеты). Служба/сервис и юнит в статье используем как синонимы, когда речь про программу-демон.


Часть I. Что такое systemd и почему он уже на вашем сервере

Когда включают компьютер, операционная система загружает ядро (kernel) и… ядру нужно, чтобы кто-то продолжил загрузку: смонтировал диски, поднял сеть, запустил демонов, а при выключении — аккуратно всё погасил. Этот «кто-то» — первый процесс с идентификатором PID 1. Раньше эту роль играл крохотный init; в современных Ubuntu/Debian (и почти везде) это systemd.

1. Чем systemd полезна системному администратору

systemd объединяет под одним управлением то, что раньше делалось кучей разрозненных утилит:

  • Запуск сервисов и зависимость между ними. «Не поднимай nginx, пока не готова сеть» — systemd сам выстроит порядок.
  • Автостарт после перезагрузки. Сервис один раз «включили» — и он живёт при каждой загрузке, без вашего участия.
  • Автоперезапуск после падения. Прописали Restart= — упавшую программу systemd сам поднимет.
  • Единое хранилище логов (журнал). Все сервисы пишут в общий journal (журнал systemd), который читают одной командой journalctl, а не ходят по разным .log-файлам.
  • Учёт ресурсов и изоляция. Каждому сервису можно ограничить память, дать отдельного пользователя и закрыть доступ к чужим файлам (этим пользуемся в части IV, когда будем настраивать PHP-FPM и изоляцию сайтов).

Для сайта на одной машине вам почти никогда не понадобится «придумывать» systemd с нуля — у nginx, PHP-FPM, MariaDB юнит-файлы уже есть из коробки. Ваша задача — уметь их читать, управлять ими через systemctl и, когда понадобится, написать свой. Поэтому дальше мы не будем пересказывать документацию, а будем трогать реальные команды, которые вы будете вводить, уже будучи deploy с sudo из прошлой статьи.

2. Первый знакомство: systemctl status systemd

systemd управляет и собой тоже. Проверим, что он жив и в каком режиме (режим — «targets», см. Часть IV):

# показать состояние менеджера и сколько юнитов активно
systemctl status --no-pager | head -n 12
# сколько всего юнитов systemd видит сейчас
systemctl list-units --type=service --all | tail -n 1

Вы увидите, что systemd уже держит десятки сервисов (сеть, логи, cron, тот же SSH). Это и есть «системный контур»: то, что живёт вне всяких сайтов. Ваша nginx и PHP-FPM просто встанут в этот ряд, когда вы их поставите.

3. Где живут файлы-юниты (и какой редактировать)

Юнит-файлы — текстовые, с расширением .service. Их несколько слоёв, и порядок наложения важен (systemd читает их от «родных» к «переопределениям»):

ПутьКто там живётТронете ли вы
/lib/systemd/system/ (или /usr/lib/systemd/system/)Файлы из пакетов, ставит apt. Перезапись при обновлении пакета.⛔ не правим руками
/etc/systemd/system/Ваши файлы и переопределения администратора✅ правим здесь
/run/systemd/system/Генерируемые на летуне трогаем

Правило: никогда не редактируйте /lib/systemd/system/nginx.service. Обновите пакет nginx — и ваша правка затрётся. Правильный способ поменять чужой юнит — systemctl edit (он создаст drop-in «перекрытие» в /etc, см. Часть V).


Часть II. Анатомия юнит-файла: три секции, которые надо понимать

Юнит-файл — это INI-подобный текст. В нём три «блока»-секции, и каждый отвечает за свою половину жизни сервиса. Возьмём реальный, упрощённый юнит PHP-воркера (такой может пригодиться для ai-news-autopilot, если его вынести из WP-Cron в отдельный процесс) и разберём построчно:

# /etc/systemd/system/qwen-news-worker.service
[Unit]
Description=Qwen News Worker: RSS → ИИ → публикация
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/example.com
ExecStart=/usr/bin/php /var/www/example.com/wp-cli/worker-news.php
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Секция [Unit] — описание и связи «до старта»

  • Description= — человекочитаемое имя; попадёт в systemctl status и в журнал. Делайте его полезным, а не myapp.
  • After= — порядок запуска (внимание: не «зависимость»!). «Стартуй меня только после того как поднялась сеть». Если сети в этом списке нет — просто нет, ничего страшного, но если она стартует — она стартует раньше нас.
  • Wants= — «я хочу, чтобы это было живым, но если не удалось — не валя меня с собой». Мягкая зависимость. Вместе After=network-online.target + Wants=network-online.target — стандартная пара: «подожди интернет и попроси его».

Разница между «порядком» (After/Before) и «зависимостью» (Wants/Requires) — самая частая причина путаницы; вернёмся к ней в Части IV, потому что новички думают, что After= уже и гарантирует работу соседа, а он нет.

Секция [Service] — как именно жить процессу

Это сердце файла. Разберём строки:

  • Type= — как systemd определяет, что сервис «успешно поднялся». Главный источник граблей. Четыре значения, которые реально нужны:
    • simple (по умолчанию) — процесс, который вы стартанули, он и есть сервис. systemd считает его запущенным сразу. Подойдёт для программ, которые сами крутятся в foreground (переднем плане), не «сваливаются в фон».
    • forking — программа при старте отпочковывает дочерний процесс (демонизируется) и сам родитель завершается. systemd по PIDFile= должен найти нового главного. Так живут классические демоны вроде старого nginx.
    • notify — программа сама отправляет systemd сигнал «я готов» (sd_notify). Требует кооперации программы; для большинства самописных скриптов не нужно.
    • oneshot — одноразовая задача, «доделать и выйти» (аналог cron-скрипта без цикла). Для цикла-воркера не годится. Грабли с Type — тема Части VI, там же и pidfile.
  • User= / Group= — под какой учётной записью запустить процесс. ⚠️ Без User= процесс стартует от root (а вам это почти всегда не надо, особенно для PHP-скриптов, которые пишут в файлы).
  • WorkingDirectory= — папка, в которой программа окажется «как бы сидя» (её текущий каталог). Если код пишет logs/app.log (относительный путь) или ищет wp-config.php по относительному пути — это будет внутри WorkingDirectory, а не там, откуда systemd.
  • ExecStart= — одна команда, которую systemd выполнит как главный процесс. ⚠️ Здесь нужен абсолютный путь (/usr/bin/php, не php) и не работают конструкции shell (cd, &&, |, *, ~). systemd сам исполняет бинарник, без bash-а между.
  • Restart= — что делать после завершения процесса. on-failure — перезапустить только при падении (ненулевой код возврата / сигнал); always — и при «честном» останове тоже (редко нужно).
  • RestartSec= — пауза перед перезапуском (защита от «цикла смерти», когда процесс падает сразу же после старта и systemd бесконечно поднимает).

Секция [Install] — как вписаться в автозагрузку

WantedBy=multi-user.target — «когда меня enable-нули, прилепи меня к multi-user.target (целевое состояние «система полностью загружена» — аналог прежнего «runlevel 5 для сервера» без графики)». Без секции [Install] команду systemctl enable к юниту не прилепить — enable читает именно [Install]. Поэтому даже «свой» сервис, который вы хотите включить в автостарт, обязан иметь [Install] (или Alias=).

Важная деталь про «связки»: enable фактически создаёт симлинк (ссылку-файл-стрелку) в папке .../multi-user.target.wants/. systemd видит юнит не из-за того, что вы положили файл в /etc, а из-за того, что на него указывает ссылка. Отсюда — enable vs daemon-reload (Часть IV).


Часть III. start и enable — две оси, которые путают все новички

Это самый полезный раздел статьи, потому что на одной машине именно эта путаница порождает «почему после перезагрузки всё пропало».

1. Две независимые команды

КомандаЧто делаетВлияет на автостарт?
systemctl start nginxЗапустить сервис прямо сейчас❌ нет
systemctl stop nginxОстановить сейчас❌ нет
systemctl enable nginxПрописать автостарт при загрузке✅ да
systemctl disable nginxУбрать автостарт при загрузке✅ (удаляет)

Можно получить четыре комбинации, и все они легальные. Типичные ошибки:

  • start без enable — сервис работает, но после reboot его нет. Классическая причина «вчера всё было, сегодня упало».
  • enable без start — сервис в автозагрузке, но сейчас не запущен (живёт только после первой перезагрузки). Часто так делают намеренно: «не трогать живой сайт, в автозагрузку включим, перезагрузимся планово».

Проверять состояние удобно раздельными командами (каждая печатает одно слово — так не ошибётесь глазами):

systemctl is-active nginx     # active | inactive | failed
systemctl is-enabled nginx    # enabled | disabled | static | ...

2. Команда-спаситель: enable --now

Чтобы включить автостарт и сразу запустить одной командой (как «поставь и стартуй»), есть --now:

# включить в автозагрузку и запустить прямо сейчас
systemctl enable --now nginx
# обратная операция: остановить и убрать из автозагрузки
systemctl disable --now nginx

3. Что делать, если вы редактировали юнит-файл

После любой правки файла сервиса (или после того, как добавили новый файл) systemd не перечитывает папку автоматически — надо сказать ему:

# перечитать все юнит-файлы systemd'у (без перезагрузки)
sudo systemctl daemon-reload
# после reload можно уже делать start/restart
sudo systemctl restart nginx

⚠️ Классический баг новичка: поправил ExecStart, сделал restart — и «ничего не изменилось», потому что без daemon-reload systemd использует старую кэшированную версию юнита. restart перезапускает процесс, а daemon-reload перечитывает конфиги юнитов. Иногда нужны оба, в этом порядке: daemon-reload → restart.

4. Ещё команды, которые пригодятся каждый день

systemctl status nginx       # состояние + последние строчки журнала + код ошибки (главный инструмент)
systemctl restart nginx      # остановить и запустить (прерывает соединения!)
systemctl reload nginx       # «мягко» перечитать конфиг без разрыва (если сервис поддерживает SIGHUP)
journalctl -u nginx -n 50    # последние 50 строк лога сервиса

restart vs reload — тоже не одно и то же: restart реально гасит процесс (обрываются активные соединения, на секунду сайт лежит), reload просит процесс сам перечитать конфиг без разрыва (nginx умеет, PHP-FPM тоже). Для продакшн-сайта на reload ориентируйтесь, когда надо применить конфиг без простоя — но не все программы понимают команду reload.


Часть IV. Порядок и зависимости: как не запускать раньше соседа

Вы читаете, что PHP-FPM не стартует без сети, а nginx не стартует без готового PHP-сокета. systemd умеет гарантировать порядок и живость соседей — но путать эти два понятия нельзя.

1. After/Before — только про порядок

Эти директивы делают только одно: если оба юнита стартуют в этом запуске — указывают очередность. After=network-online.target означает «если сеть тоже поднимается сейчас — подожди её», но не «гарантирую, что сеть поднялась, иначе не стартую». Сама сеть не является зависимостью — systemd просто ставит её в порядок раньше нас, но не обязывает её быть живой.

Отсюда пара, которую вы уже видели в Части II:

After=network-online.target
Wants=network-online.target

After= ставит порядок; Wants= просит «включи сеть как соседа». Вместе — «подожди сеть, и пусть она тоже попытается подняться».

2. Requires/Wants — зависимость живости

  • Wants= — мягкая зависимость: «попроси этот юнит тоже стартануть». Если тот упал — нас не валяет.
  • Requires= — жёсткая: «если этот не стартанул (или упал) — я тоже останавливаюсь». Почти всегда это слишком: база упала — и ваш сервис умирает. Держите Requires для случаев, где связь буквально жизненная (например, сокет-активация).

Есть и обратные BindsTo=/PartOf= — тонкие, на одной машине без оркестрации вам почти не нужны; если возникнет ситуация «сервис должен умирать вместе с другим», почитайте man systemd.unit, там таблица всех связей.

3. Целевые юниты (target) и WantedBy

target — «состояние системы», группа юнитов, которые должны быть живы в этом состоянии. Вам хватит двух, о которых стоит знать:

  • multi-user.target — «сервер загрузился, всё консольное готово, графики нет». Именно сюда прилепляют почти все демоны (это ваш дефолт).
  • network-online.target — «сеть не просто поднята, а реально есть связь» (отличает «интерфейс есть» от «есть интернет»).

WantedBy=multi-user.target в [Install] говорит systemd: при enable создай симлинк в папку multi-user.target.wants/ — и в автостарту сервис будет включён как часть «загруженного сервера».

4. Практический случай: два сервиса, один должен дождаться другого

Допустим, вы хотите гарантировать «PHP-FPM готов, прежде чем nginx стартует» (у вас nginx ходит к PHP через unix-сокет, см. часть III серии). В юните nginx есть After=/Requires= к PHP-FPM из коробки. А в своём сервисе, которому нужен живой PHP-FPM, вы бы написали:

[Unit]
After=php8.2-fpm.service
Wants=php8.2-fpm.service

Имя соседа узнаёте из systemctl status (у разных версий — php7.4-fpm, php8.1-fpm, php8.3-fpm — смотрите фактическое через systemctl list-units | grep fpm). Сам приём фильтрации вывода через grep и регулярки (как написать паттерн, который ловит имена всех fpm-юнитов сразу) — в Регулярные выражения на практике.

⛔ Никогда не вешайте сетевые вызовы/is_user_logged_in()/setcookie() в логику, которая выполняется в момент загрузки юнита-процесса. Держите инициализацию lazy (ленивой), внутри рабочего цикла. systemd не простит падения в первые секунды, а вы получите стартовый фатал в журнале. (В контексте плагинов WP это то же самое: конструктор PHP-класса без сетевой работы; в контексте systemd — процесс должен уметь «не делая ничего опасного» пережить сам старт.)


Часть V. Диагностика: как понять, почему сервис упал

Сервис лёг. Сайт лежит. Вы в панике вводите systemctl restart вслепую — и это ошибка, потому что перезапуск не лечит причину, а следующая диагностика станет темнее. Правильный путь — сначала прочесть причину, а не перезапускать вслепую.

1. systemctl status — первое и главное окно

systemctl status nginx --no-pager

Типичный вывод и как его читать:

● nginx.service - A high performance web server and reverse proxy
     Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
     Active: failed (Result: exit-code) since ...
    Process: 1412 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=1/FAILURE)
   Main PID: 1412 (code=exited, status=1/FAILURE)
        CPU: 18ms

Три красные зацепки:

  • Active: failed — сервис мёртв.
  • (Result: exit-code) — программа завершилась сама с ненулевым кодом (это чаще всего ваша ошибка: битый конфиг, нет файла, нет прав, кривой путь в ExecStart).
  • status=1/FAILURE — конкретный код возврата процесса. Код 1 = «я отвалился». 2 = часто misuse of shell builtins / «не понял аргументы».

Другие Result, которые сразу говорят, где копать:

  • Result: signal (например status=killed, code=signal) — процесс убит сигналом извне (OOM-killer из прошлой статьи — частый случай: code=killed, status=9/KILL = «забило нехваткой памяти»).
  • Result: timeout — systemd ждал, пока сервис «поднимется», а он не сказал «я готов» → таймаут (грабель про Type=/notify/pidfile).
  • Result: start-limit-hit — «ты падал слишком часто за короткое время, systemd больше не поднимает». Лечится разбором причины + systemctl reset-failed (см. ниже).
  • Result: exit-code с кодом 203/EXEC — «не найден бинарник в ExecStart» (частая ошибка после правки пути).

2. journalctl — лог сервиса целиком

status показывает последние строчки. Полная картина — в журнале:

# весь журнал сервиса (страницей, вниз)
journalctl -u nginx.service
# хвост + «следить» как tail -f
journalctl -u nginx.service -f
# последние 100 строк
journalctl -u nginx.service -n 100
# только ошибки текущего ядра за сегодня (английские приоритеты: err=3)
journalctl -u nginx.service -p err --since today
# журнал за текущую загрузку системы (после reboot — не путать с прошлой!)
journalctl -b -u nginx.service

Флаги: -u (unit, сервис), -b (boot, текущая загрузка), -n (сколько строк), -f (follow), -p (приоритет), --since/--until. Полезно: journalctl -k — журнал ядра (там видно, например, OOM-killer по имени процесса).

3. Коды-подсказки → действия

Что видите в status/journalctlПочти наверняка причинаЧто делать
status=203/EXECнет бинарника/путь в ExecStart неверныйпроверить путь command -v, поставить абсолютный
status=1/FAILURE, лог «config test failed»битый конфиг программыпрогнать проверку конфига (nginx -t, php -l)
status=200/CHDIRWorkingDirectory не существуетуказать реальную папку
status=217/USERпользователь в User= не существуетзавести пользователя или поправить имя
status=9/KILL, «Out of memory»память кончилась (OOM)free -h, swap (из Части V прошлой), лимиты PHP-FPM
status=timeoutне сказал «готов», неверный Type=поправить Type= (forking/simple/notify), PIDFile=
start-limit-hitпадал > N раз за окночинить причину → systemctl reset-failed nginx

4. Сброс «я устал его поднимать» и переопределение конфига

Когда сервис ловит start-limit-hit, systemd перестаёт его рестартить. После того как починили причину, сбросьте «счётчик падений»:

sudo systemctl reset-failed nginx
sudo systemctl start nginx

Править чужой юнит из /lib нельзя (перетрётся). Правильно — drop-in override (перекрытие), который systemd сложит поверх оригинала:

# создаст редактируемый overlay в /etc/systemd/system/nginx.service.d/override.conf
sudo systemctl edit nginx

В открывшемся окне:

[Service]
# дописать/добавить параметр; пустой ExecStart= «очищает» предыдущий, затем задаём свой
Environment="CURL_ALL=1"

После сохранения systemd сам делает reload (он же его вызывающий systemctl edit умеет это). Так добавляются «наладочные» переопределения без порчи пакета — и это единственный долгий путь, который переживёт apt upgrade nginx.


Часть VI. Собираем свой юнит: воркер, который должен пережить ночь

Доведём до конца: вы написали PHP-скрипт, который раз в 30 секунд опрашивает очередь/RSS и что-то делает (у вас похожее есть в ai-news-autopilot). WP-Cron — штука капризная (зависит от трафика), а системный демон живёт независимо. Оформим это как юнит systemd и разберём, где новички грабли ловят.

1. Скрипт-цикл

PHP
<?php
// /var/www/example.com/wp-cli/worker-news.php
// Бесконечный цикл: сделать работу — поспать — повторить.
// Задаём часовой пояс, чтобы логи не «плыли» (выбор из прошлой статьи).
date_default_timezone_set('Europe/Moscow');
while (true) {
    try {
        // TODO: ваша работа — опрос очереди, вызов модели, публикация.
        // ВАЖНО: никаких сетевых действий в первой строке до старта цикла.
        sleep(30);
    } catch (\Throwable $e) {
        // Не убиваем демона на одной упавшей итерации: логируем и идём дальше.
        fwrite(STDERR, $e->getMessage() . "\n");
        sleep(30);
    }
}

Ключевой момент для systemd: процесс сам держится foreground-циклом. Именно поэтому у нас Type=simple (в отличие от демонизированного nginx с forking + pidfile).

2. Юнит

# /etc/systemd/system/qwen-news-worker.service
[Unit]
Description=Qwen News Worker (RSS → AI → publish)
After=network-online.target mariadb.service
Wants=network-online.target

[Service]
Type=simple
User=deploy
Group=deploy
WorkingDirectory=/var/www/example.com
ExecStart=/usr/bin/php /var/www/example.com/wp-cli/worker-news.php
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=5
# ограничение памяти: не дам воркеру сожрать всю машину (OOM-killer пострашнее будет)
MemoryMax=300M

[Install]
WantedBy=multi-user.target

Пояснения к «грабельным» строкам:

  • Type=simple — процесс в foreground; systemd считает его живым сразу, как только ExecStart запустился.
  • User=deploy — скрипт запускается не от root (иначе пишет файлы root’у и ломает права для PHP-FPM, см. Часть V прошлой про владельца authorized_keys).
  • WorkingDirectory= — относительные пути внутри кода (require './wp-load.php') резолвятся здесь.
  • ExecStart= — абсолютный путь к php и скрипту (/usr/bin/php, взять из command -v php). Никаких cd ... && php — shell-конструкции не работают.
  • Restart=on-failure + RestartSec=5 + StartLimitBurst=5 за 60 сек — если скрипт упал, systemd поднимет, но если падает больше 5 раз за минуту — остановится и поставит start-limit-hit (чтобы не жог CPU в вечном цикле). После этого — reset-failed.
  • MemoryMax=300M — жёсткий потолок памяти для этого процесса; при превышении systemd убьёт воркер (Result: signal), не давая положить всю машину. Числа под свой VPS (у вас память дефицит, см. swap из Части V прошлой).

3. Включение и проверка

# файл положили в /etc/systemd/system/ — теперь скажем systemd его заметить
sudo systemctl daemon-reload
# включить в автостарт и запустить сейчас
sudo systemctl enable --now qwen-news-worker
# убедиться, что живёт и не в failed
systemctl is-active qwen-news-worker      # active
systemctl status qwen-news-worker --no-pager
# последние строки его лога
journalctl -u qwen-news-worker -n 30 --no-pager

4. Разбор готового чужого юнита (nginx) — «как читать, а не писать»

Полезное упражнение перед третьей статьей серии:

# показать, что за юнит реально загружен (с учётом drop-in)
systemctl cat nginx.service --no-pager

Вы увидите в [Service] ExecStart=/usr/sbin/nginx -g 'daemon off; master_process on;' — то есть в Ubuntu/Debian nginx работает через ExecStart с daemon off;, а значит Type=simple (или notify), а не старый добрый forking с pidfile. Если бы вы пытались править nginx-юнит и думали, что он forking — ошиблись. Читая cat, вы это видите за секунду. Это же упражнение проделайте с php8.2-fpm (там обычно Type=simple + NotifyAccess) и mariadb (там Type=notify + свой /run/mysqld/mysqld.pid-подобный механизм) — и вы перестанете бояться чужих юнитов.

Инструменты под задачу

Опишите свой сервис (что за процесс, путь к бинарнику, от какого пользователя, нужен ли автозапуск) — генератор соберёт корректный .service по чек-листу граблей из этой статьи. А если юнит уже есть и что-то падает — вставьте его в режим «Проверить готовый»: ИИ разберёт ExecStart, Type=, Restart=, WantedBy= и зависимости и покажет, что чинить.

AI Systemd Unit Generator
Гость
Токены: 0 / 50 000
Гостевой лимит — 50 000 токенов. Зарегистрируйтесь — лимит + 200 000.
0 / 16 000 символов

Результат
Опишите сервис слева (или возьмите пресет) и нажмите «Собрать юнит».
ИИ сгенерирует корректный .service по чек-листу граблей из статьи или проверит вставленный вами юнит.

Чек-лист граблей этой части

  • `ExecStart` без абсолютного пути (`php` вместо `/usr/bin/php`) → `status=203/EXEC`. Путь берите из `command -v`.
  • shell-конструкции в `ExecStart` (`cd`, `&&`, `|`, `~`) → игнорируются/ломаются, systemd исполняет бинарник напрямую. Нужен shell — оборачивайте явно: `ExecStart=/bin/bash -c ‘…’` (но это костыль, лучше абсолютные пути).
  • забыли `daemon-reload` после правки файла → рестарт использует старую версию, «ничего не изменилось».
  • `start` без `enable` → сервис живой, а после `reboot` его нет («всё упало после перезагрузки»).
  • `enable` без `[Install]`/`WantedBy` → `enable` не прилепит автостарт, или сервис станет `static`.
  • `After=` считали зависимостью → сервис стартанул раньше соседа и получил «connection refused» (нужен и `Wants=`/`Requires=`).
  • нет `WorkingDirectory` → относительные пути (`logs/`, `./wp-load.php`) ищутся где-то не там; часто `/`.
  • `User=` забыт→ процесс от root, файлы-артефакты становятся root-owned, и PHP-FPM потом не может их прочитать/записать.
  • `Restart=always` вместо `on-failure` → сервис перестанет останавливаться по вашей команде `stop` (он всегда поднимется).
  • правка `/lib/systemd/system/…` вместо `systemctl edit` → `apt upgrade` затрёт вашу настройку, и вы неделя спустя не поймёте, куда делся override.
  • `Type=notify` при скрипте без sd_notify → `Result: timeout`, сервис «не сказал что готов» и убит по таймауту.
  • нет `MemoryMax`/`RestartSec` при падающем цикле → воркер уходит в бесконечный рестарт-шторм или жрёт память; нужен `StartLimitBurst`/`RestartSec` и потолок памяти.

Материал написан простым языком; любой англоязычный термин (systemd, демон, юнит, target, drop-in, journal, foreground) объясняется при первом употреблении. Команды и директивы — для Ubuntu/Debian с systemd и apt; конкретные номера версий пакетов/юнитов (например, php8.2-fpm против php8.3-fpm) опускаются сознательно — проверяйте фактическое имя через systemctl list-units. Все домены и пути в примерах условные (example.test, /var/www/example.com); перед копированием подставьте свои. Тонкие механизмы (BindsTo=, PartOf=, ExecStartPost=, socket-активация) упомянуты, но не раскрыты — это уровень man systemd.service / man systemd.unit, когда понадобится.

👤 Зарегистрируйтесь, чтобы добавить материал в закладки.