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
- Étape 1 : savoir ce qui ne tourne pas maintenant
- Étape 2 : bien régler Restart= (avec un drop-in, pas dans l'unité elle-même)
- Étape 3 : OnFailure= — une alerte vers votre téléphone, intégrée
- Étape 4 : les services bloqués — le watchdog
- Étape 5 : le script cron qui comble les trous
- Pièges
- Ce qu'il vous manque encore
- Comment monsys fait
- 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=alwayssur un oneshot. Un script de sauvegarde avecRestart=alwaysse relance sans fin.Restart=est pour les démons ; pour les tâches, utilisez des timers.- Un
ExecStartPrequi échoue compte pour la limite de démarrage. Unmkdirqui é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 restartdans 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) : avecRestart=, d'anciens workers traînent parfois siKillMode=process. La valeur par défaut (control-group) est presque toujours la bonne.- Oublier
daemon-reload. Un drop-in sanssystemctl daemon-reloadest un fichier texte.systemctl showvous dit ce qui est vraiment actif.
Ce qu'il vous manque encore
- L'historique.
NRestartsse 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
aptde 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.