Supervision de serveursdébutant5 min de lecture

Surveiller et redémarrer automatiquement les services systemd : Restart=, OnFailure=, watchdogs — et quand ça ne suffit pas

Un service qui plante est de retour en quelques secondes avec Restart=on-failure. Un service qui pend, un qui tourne en boucle de redémarrage, ou un qui n'a jamais démarré après un reboot, systemd ne le voit pas tout seul. Voici les réglages en drop-in, le gestionnaire OnFailure qui pousse vers votre téléphone, l'astuce WatchdogSec pour les processus bloqués et le script cron qui comble les trous.

Sommaire
  1. Étape 1 : savoir ce qui ne tourne pas maintenant
  2. Étape 2 : bien régler Restart= (avec un drop-in, pas dans l'unité elle-même)
  3. Étape 3 : OnFailure= — une alerte vers votre téléphone, intégrée
  4. Étape 4 : les services bloqués — le watchdog
  5. Étape 5 : le script cron qui comble les trous
  6. Pièges
  7. Ce qu'il vous manque encore
  8. Comment monsys fait
  9. FAQ

La plupart des tickets « le service est down » ont une cause banale : le processus a planté à 03:12 et personne n'avait configuré Restart= ; ou il tournait, mais ne répondait plus ; ou il était simplement arrêté depuis le reboot de la semaine dernière parce que personne n'avait fait enable. systemd peut attraper les trois — mais pas avec les réglages par défaut, et pas sans que vous réfléchissiez à ce que « sain » signifie pour ce service.

Étape 1 : savoir ce qui ne tourne pas maintenant

# Unités en état failed (crash, exit ≠ 0, limite de démarrage atteinte)
systemctl --failed
# Services activés (enabled) mais qui NE tournent PAS (p. ex. pas revenus après un reboot)
systemctl list-unit-files --type=service --state=enabled --no-legend | awk '$1 !~ /@\.service$/ {print $1}' \
  | while read -r u; do
      # les oneshots et les unités dont la Condition n'est pas remplie (cloud-init sur du bare metal) doivent être inactives
      [ "$(systemctl show "$u" -p Type --value)" = oneshot ] && continue
      [ "$(systemctl show "$u" -p ConditionResult --value)" = no ] && continue
      # terminé proprement (Result=success, p. ex. cloud-init-main) n'est pas une panne ; un crash donne exit-code/signal
      [ "$(systemctl show "$u" -p Result --value)" = success ] && ! systemctl is-active --quiet "$u" && continue
      systemctl is-active --quiet "$u" || echo "enabled-but-inactive: $u"
    done
# Combien de fois un service a-t-il redémarré depuis le boot ?
systemctl show nginx -p NRestarts
# Pourquoi a-t-il échoué ?
journalctl -u nginx -b -p warning --no-pager | tail -20

Cette deuxième liste est la surprenante : des unités « enabled » mais inactives. Les oneshots, les unités modèles et celles dont la Condition… n'est pas remplie (cloud-init sur une machine physique) ont leur place là ; un serveur web, non — c'est pourquoi la boucle filtre ces trois cas.

Étape 2 : bien régler Restart= (avec un drop-in, pas dans l'unité elle-même)

Ne modifiez jamais /lib/systemd/system/*.service — une mise à jour du paquet l'écrase. Un drop-in dans /etc/systemd/system/<unit>.service.d/ survit à tout.

sudo systemctl edit nginx
[Unit]
# Limite de démarrage : par défaut 5 tentatives en 10 s, puis failed et PLUS RIEN. Trop serré pour
# un service qui attend une base de données. Fenêtre plus large.
StartLimitIntervalSec=300
StartLimitBurst=10

[Service]
# on-failure : sur crash ou exit ≠ 0. always : aussi sur exit 0 propre (pour les démons qui se croient « finis »).
Restart=on-failure
RestartSec=5s

Attention à la section : StartLimitIntervalSec et StartLimitBurst vont dans [Unit] (depuis systemd 230) ; les anciens exemples les mettent dans [Service], ce qui génère un avertissement et est ignoré sur les versions récentes.

Vérifiez ensuite ce que systemd utilise réellement :

sudo systemctl daemon-reload
systemctl show nginx -p Restart -p RestartUSec -p StartLimitBurst -p StartLimitIntervalUSec

Pour les services avec dépendances (une application qui a besoin d'une base de données) : After=postgresql.service dans [Unit], et un retry de connexion dans l'application elle-même. Requires= semble logique mais fait s'arrêter votre application quand vous redémarrez la base — rarement ce que vous voulez.

Étape 3 : OnFailure= — une alerte vers votre téléphone, intégrée

systemd peut démarrer une autre unité quand un service échoue. Une unité de notification générique pour tous les services :

sudo tee /etc/systemd/system/notify-failed@.service >/dev/null <<'EOF'
[Unit]
Description=Push failure of %i to ntfy

[Service]
Type=oneshot
# %i = le nom de l'unité en échec (via OnFailure=notify-failed@%n.service)
ExecStart=/bin/sh -c 'journalctl -u %i -n 15 --no-pager -o cat | curl -s -H "Title: FAILED %i on $(hostname -s)" -H "Priority: high" --data-binary @- https://ntfy.example.be/services >/dev/null'
EOF

# Rattacher à un service via drop-in
sudo systemctl edit nginx
[Unit]
OnFailure=notify-failed@%n.service

%n est le nom complet de l'unité (nginx.service) ; il devient l'instance de l'unité de notification. Test avec une unité jetable :

sudo systemd-run --unit=crashtest -p OnFailure=notify-failed@crashtest.service /bin/false
# → push avec les 15 dernières lignes de journal de crashtest

Attention : OnFailure ne se déclenche que quand l'unité échoue vraiment — donc après avoir atteint la limite de démarrage, pas à chaque crash individuel absorbé par Restart=. C'est exactement ce qu'il faut : pas un push par redémarrage, mais un seul quand redémarrer n'aide plus.

Étape 4 : les services bloqués — le watchdog

Restart= réagit à un processus qui s'arrête. Un processus coincé dans un deadlock, tous les threads occupés, ne s'arrête pas. Pour les services qui supportent sd_notify de systemd (nginx non ; beaucoup de démons Go/Rust/Python oui, et tout ce qui communique via systemd-notify), il y a WatchdogSec= :

[Service]
Type=notify
WatchdogSec=30s
# Si le processus n'envoie pas WATCHDOG=1 toutes les 30 s, il reçoit SIGABRT et systemd le redémarre
Restart=on-watchdog

Si votre service ne le supporte pas, construisez le watchdog en dehors du service : un timer qui fait une vraie requête et redémarre en cas d'échec.

sudo tee /etc/systemd/system/healthcheck-web.service >/dev/null <<'EOF'
[Unit]
Description=HTTP healthcheck for the web app; restart on failure
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'curl -fsS -m 5 -o /dev/null http://127.0.0.1:8080/healthz || { echo "healthz failed, restarting"; systemctl restart webapp.service; }'
EOF
sudo tee /etc/systemd/system/healthcheck-web.timer >/dev/null <<'EOF'
[Unit]
Description=Run web healthcheck every minute
[Timer]
OnBootSec=2min
OnUnitActiveSec=1min
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload && sudo systemctl enable --now healthcheck-web.timer

Deux règles pour un tel watchdog externe : le healthcheck doit faire quelque chose de réel (une requête, pas seulement « 200 OK »), et le redémarrage doit être journalisé et compté — sinon vous masquez une fuite mémoire en « redémarrage occasionnel ».

Étape 5 : le script cron qui comble les trous

Ce que systemd ne signale pas lui-même : les unités enabled-mais-inactives, les boucles de redémarrage qui restent juste sous la limite, et les services qui ne sont pas revenus après un reboot.

sudo tee /usr/local/sbin/service-check.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
NTFY="https://ntfy.example.be/services"
HOST=$(hostname -s)
STATE=/var/lib/service-check.state
touch "$STATE"; MSG=()

# 1. Unités en échec
f=$(systemctl --failed --no-legend | awk '{print $1}' | paste -sd, -)
[ -n "$f" ] && MSG+=("failed: $f")

# 2. Activées mais inactives (modèles, oneshots et unités à Condition non remplie exclus)
while read -r u; do
  [ "$(systemctl show "$u" -p Type --value)" = oneshot ] && continue
  [ "$(systemctl show "$u" -p ConditionResult --value)" = no ] && continue
  systemctl is-active --quiet "$u" && continue
  # Terminé proprement (Result=success) = tâche de boot comme cloud-init-main, pas une panne.
  # Un démon qui s'arrête inopinément avec exit 0, vous l'attrapez avec Restart=always (étape 2).
  [ "$(systemctl show "$u" -p Result --value)" = success ] && continue
  MSG+=("enabled-but-inactive: $u ($(systemctl show "$u" -p Result --value))")
done < <(systemctl list-unit-files --type=service --state=enabled --no-legend | awk '$1 !~ /@\.service$/ {print $1}')

# 3. Boucles de redémarrage : NRestarts augmenté de ≥ 3 depuis le contrôle précédent (10 min)
while read -r u; do
  n=$(systemctl show "$u" -p NRestarts --value); [ "${n:-0}" -eq 0 ] && continue
  prev=$(grep "^$u " "$STATE" | awk '{print $2}'); prev=${prev:-0}
  [ $((n - prev)) -ge 3 ] && MSG+=("restart-loop: $u ($((n - prev)) restarts in 10 min, total $n)")
  sed -i "/^$u /d" "$STATE"; echo "$u $n" >> "$STATE"
done < <(systemctl list-units --type=service --state=running --no-legend | awk '{print $1}')

[ ${#MSG[@]} -gt 0 ] && printf '%s\n' "${MSG[@]}" | curl -s -H "Title: services@$HOST" -H "Priority: high" --data-binary @- "$NTFY" >/dev/null
EOF
sudo chmod 0755 /usr/local/sbin/service-check.sh
echo '*/10 * * * * root /usr/local/sbin/service-check.sh' | sudo tee /etc/cron.d/service-check

Pièges

  • Restart=always sur un oneshot. Un script de sauvegarde avec Restart=always se relance sans fin. Restart= est pour les démons ; pour les tâches, utilisez des timers.
  • Un ExecStartPre qui échoue compte pour la limite de démarrage. Un mkdir qui échoue parce qu'un montage manque brûle vos dix tentatives en quelques secondes. Préfixez la commande de - (ExecStartPre=-/bin/mkdir …) si l'échec est acceptable.
  • systemctl restart dans un healthcheck sans limitation. Si la base de données est absente, vous redémarrez chaque minute une application qui ne marchera pas de toute façon, et vous remplissez les logs. Comptez les redémarrages et arrêtez après trois : c'est alors une alerte, pas un correctif.
  • KillMode= et les enfants. Un service qui lance des workers (gunicorn, php-fpm) : avec Restart=, d'anciens workers traînent parfois si KillMode=process. La valeur par défaut (control-group) est presque toujours la bonne.
  • Oublier daemon-reload. Un drop-in sans systemctl daemon-reload est un fichier texte. systemctl show vous dit ce qui est vraiment actif.

Ce qu'il vous manque encore

  • L'historique. NRestarts se remet à zéro à chaque boot. « Combien de fois ce service a-t-il redémarré ces 30 derniers jours » demande un journal que vous tenez vous-même.
  • La vue de flotte. Trente serveurs, trente listes --failed, trente topics ntfy.
  • Le contexte. Le service qui a planté à 03:12 et la mise à niveau apt de 03:10 sont dans deux logs ; la corrélation est manuelle.

Comment monsys fait

L'agent monsys lit l'état systemd de chaque hôte : unités en échec, enabled-mais-inactives, NRestarts, événements de redémarrage horodatés, et le statut de sortie. Les boucles de redémarrage et les services down deviennent des alertes dédupliquées avec les lignes du journal et les métriques de l'hôte de ce moment à côté ; le moteur SLA calcule par service un pourcentage de disponibilité sur des tranches de 5 minutes, de sorte que « 99,7 % le mois dernier » est un fait et non une estimation. Redémarrer reste une action qu'un humain approuve — via un Emergency Action Token signé, avec la sortie de systemctl restart dans le journal d'audit.

FAQ

systemd redémarre-t-il automatiquement un service après un crash ?

Seulement si Restart= est configuré (on-failure ou always) ; la valeur par défaut est no. Et même alors, systemd s'arrête après StartLimitBurst tentatives dans StartLimitIntervalSec et met l'unité en failed.

Comment voir pourquoi un service est en échec ?

systemctl status <unit> montre les dernières lignes ; journalctl -u <unit> -b -p warning le contexte complet depuis le boot. systemctl show <unit> -p Result -p ExecMainStatus donne le code de sortie et la raison (exit-code, signal, start-limit-hit, watchdog).

Le watchdog fonctionne-t-il avec tous les services ?

Seulement avec les services qui utilisent Type=notify et envoient périodiquement WATCHDOG=1 via sd_notify. Pour les autres, construisez un healthcheck externe avec un timer, comme à l'étape 4.

Rédigé par l'équipe monsys — des sysadmins qui font cela tous les jours.

Fait à la main ? Laissez monsys s'en charger en continu.

Tout ce que contient ce guide tourne dans monsys comme contrôle permanent, avec historique, alertes et preuves d'audit. 5 serveurs gratuits, hébergés en Belgique, installés en 60 secondes.