Половина инцидентов «сервис не поднимается после перезагрузки» — это не баги, а кривые зависимости в systemd-юнитах. Я насмотрелся столько ошибок типа Requires=network.target без After=network.target, что решил написать памятку для себя и для тех, кто только разбирается.

Главное, что надо понять

Зависимости делятся на две категории, и их часто путают:

  1. Порядок запускаAfter= и Before=. Говорит systemd, в каком порядке стартовать юниты.
  2. Жёсткость связиRequires=, Wants=, BindsTo=, PartOf=, Requisite=. Говорит, насколько критична зависимость и что делать, если зависимый юнит упал.

Эти два аспекта независимы. Если вы пишете Requires=foo.service — это значит «foo должен быть запущен», но НЕ значит «foo стартанёт раньше нас». Если хотите гарантировать порядок — добавляйте After=foo.service отдельно.

Простой пример, как ломается

Есть сервис, который пишет в логи через сеть. Юнит:

[Unit]
Description=Network logger
Requires=network.target

[Service]
ExecStart=/usr/local/bin/netlogger
Restart=on-failure

[Install]
WantedBy=multi-user.target

После ребута сервис в 50% случаев падает, потому что стартует одновременно с network.target, и сеть ещё не готова. Чинится так:

[Unit]
Description=Network logger
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/local/bin/netlogger
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Тут важно: network.target означает «сетевой стек поднят», а не «у нас есть IP и работает DNS». Если нужна реальная сетевая связность — используйте network-online.target и Wants= (потому что Requires= на network-online.target может затянуть весь boot).

Что значит каждая директива

After / Before

Это порядок, и только порядок. Если в After=foo.service, мы стартуем после foo — но если foo не существует или не запускается, мы всё равно стартанём. Это не зависимость, это очерёдность.

Requires

Жёсткая зависимость. Если зависимый юнит остановили — нас тоже остановят. Если зависимый юнит упал при старте — мы не стартуем. Без After= порядок не гарантирован.

[Unit]
Requires=postgresql.service
After=postgresql.service

Это «postgres обязан быть жив, иначе нас тоже выключают».

Wants

Мягкая версия Requires. Если зависимый юнит не запустился — мы всё равно стартуем. Используется чаще всего, потому что более устойчиво к каскадным сбоям.

BindsTo

Похоже на Requires, но строже: если зависимый юнит перестал быть активен по любой причине (включая нормальную остановку), нас тоже остановят. Полезно для tightly coupled пар, например, контейнер и его сетевой namespace.

PartOf

Однонаправленная привязка к перезапуску: если зависимый юнит рестартовали или остановили — нас тоже. Но если упали мы — на него это не влияет. Полезно для группировки.

Requisite

Жёстко требует, чтобы зависимый юнит был уже запущен на момент старта. Не пытается стартовать его — просто фейлится, если не активен. Редко используется.

Реальный пример с docker и WireGuard

Допустим, есть docker-контейнер с прокси, который должен слать трафик через wg-туннель. Если docker.service стартует раньше, чем wg-quick@wg0.service, то контейнер поднимется без маршрутов через туннель, и трафик пойдёт мимо.

# /etc/systemd/system/docker.service.d/override.conf
[Unit]
Wants=wg-quick@wg0.service
After=wg-quick@wg0.service network-online.target

Override через systemctl edit docker.service — лучше, чем править основной юнит. Можно делать systemctl daemon-reload без перетаскивания docker’а целиком.

Пример с conditional-сервисом

Хотим, чтобы наш сервис запускался только если есть определённый файл (например, конфиг):

[Unit]
Description=My app
ConditionPathExists=/etc/myapp/config.yml
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure

ConditionPathExists не вызовет ошибку, если файла нет — он просто скажет «нечего запускать, юнит skipped».

Отладка зависимостей

Самые полезные команды:

systemctl list-dependencies myservice.service
systemctl list-dependencies myservice.service --reverse
systemctl show myservice.service | grep -E '^(After|Before|Wants|Requires|BindsTo)='
systemd-analyze critical-chain myservice.service
systemd-analyze plot > boot.svg

critical-chain показывает цепочку зависимостей с таймингами — где именно бутстрап тормозит. plot рисует SVG с диаграммой загрузки — иногда красивее, чем смотреть в текст.

Самые частые ошибки

Ошибка 1: Requires= без After=.
Юнит зависит от postgres, но стартует одновременно с ним. В половине случаев фейлится.

Ошибка 2: After=network.target вместо After=network-online.target.
Сетевой стек поднят, но интерфейс ещё не получил IP. Сервис стартует, не может забиндиться, падает.

Ошибка 3: Restart=always без RestartSec=.
Сервис падает, рестартует через 100 мс, опять падает, опять рестарт. systemd через 5 рестартов забивает и помечает юнит как failed. Лечится RestartSec=5 и StartLimitBurst=10.

Ошибка 4: BindsTo=foo.target на пользовательский target.
Если этот target не существует, юнит не стартует с криптическим сообщением. Проверяйте, что target определён.

Итог

Запомните три вещи: After — это про порядок, Requires/Wants — это про обязательность, и эти две вещи всегда нужно указывать вместе. Если что-то не стартует после ребута — первым делом смотрите systemd-analyze critical-chain и проверяйте, что зависите от network-online.target, а не network.target.