Это вторая статья серии «Эксплуатация 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, а из-за того, что на него указывает ссылка. Отсюда —enablevsdaemon-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/CHDIR | WorkingDirectory не существует | указать реальную папку |
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
// /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= и зависимости и покажет, что чинить.
ИИ сгенерирует корректный .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, когда понадобится.
👤 Зарегистрируйтесь, чтобы добавить материал в закладки.
