systemd-services bewaken en automatisch herstarten: Restart=, OnFailure=, watchdogs — en wanneer dat niet genoeg is
Een service die crasht is met Restart=on-failure in seconden terug. Een service die hangt, een die in een restart-loop zit, of een die na een reboot nooit is gestart, ziet systemd niet vanzelf. Dit zijn de drop-in-instellingen, de OnFailure-handler die naar je telefoon pusht, de WatchdogSec-truc voor hangende processen en het cron-script dat de gaten dicht.
Inhoud
De meeste "de service is down"-tickets hebben een saaie oorzaak: het proces crashte om 03:12 en niemand had Restart= ingesteld; of het draaide wel, maar antwoordde niet meer; of het stond na een reboot van vorige week gewoon uit omdat niemand enable had gedaan. systemd kan alle drie afvangen — maar niet met de standaardinstellingen, en niet zonder dat je erover nadenkt wat "gezond" voor die service betekent.
Stap 1: weet wat er nu niet draait
# Units in failed-state (crash, exit ≠ 0, start-limit bereikt)
systemctl --failed
# Services die enabled zijn maar NIET draaien (bv. na een reboot niet opgekomen)
systemctl list-unit-files --type=service --state=enabled --no-legend | awk '$1 !~ /@\.service$/ {print $1}' \
| while read -r u; do
# oneshots en units waarvan de Condition niet klopt (cloud-init op bare metal) horen inactive te zijn
[ "$(systemctl show "$u" -p Type --value)" = oneshot ] && continue
[ "$(systemctl show "$u" -p ConditionResult --value)" = no ] && continue
# netjes beëindigd (Result=success, bv. cloud-init-main) is geen storing; een crash geeft 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
# Hoe vaak is een service herstart sinds boot?
systemctl show nginx -p NRestarts
# Waarom faalde hij?
journalctl -u nginx -b -p warning --no-pager | tail -20
Die tweede lijst is de verrassende: units die "enabled" zijn maar inactive. Oneshots, template-units en units waarvan de Condition… niet klopt (cloud-init op een fysieke machine) horen daar te staan; een webserver niet — daarom filtert de loop die drie eruit.
Stap 2: Restart= goed zetten (met een drop-in, niet in de unit zelf)
Wijzig nooit /lib/systemd/system/*.service — een package-update overschrijft het. Een drop-in in /etc/systemd/system/<unit>.service.d/ overleeft alles.
sudo systemctl edit nginx
[Unit]
# Start-limit: standaard 5 pogingen in 10 s, daarna failed en NIETS meer. Dat is te krap voor
# een service die op een database wacht. Ruimer venster.
StartLimitIntervalSec=300
StartLimitBurst=10
[Service]
# on-failure: bij crash of exit ≠ 0. always: ook bij nette exit 0 (voor daemons die "klaar" denken te zijn).
Restart=on-failure
RestartSec=5s
Let op de sectie: StartLimitIntervalSec en StartLimitBurst horen in [Unit] (sinds systemd 230); oudere voorbeelden zetten ze in [Service], wat een waarschuwing geeft en op nieuwere versies genegeerd wordt.
Controleer daarna wat systemd effectief gebruikt:
sudo systemctl daemon-reload
systemctl show nginx -p Restart -p RestartUSec -p StartLimitBurst -p StartLimitIntervalUSec
Voor services met afhankelijkheden (app die een database nodig heeft): After=postgresql.service in [Unit], en in de app zelf een retry op de connectie. Requires= klinkt logisch maar zorgt dat je app mee stopt als je de database herstart — meestal niet wat je wilt.
Stap 3: OnFailure= — een alert naar je telefoon, ingebouwd
systemd kan een andere unit starten wanneer een service failed. Eén generieke notify-unit voor alle services:
sudo tee /etc/systemd/system/notify-failed@.service >/dev/null <<'EOF'
[Unit]
Description=Push failure of %i to ntfy
[Service]
Type=oneshot
# %i = de naam van de gefaalde unit (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
# Koppel aan een service via drop-in
sudo systemctl edit nginx
[Unit]
OnFailure=notify-failed@%n.service
%n is de volledige unit-naam (nginx.service); die wordt de instance van de notify-unit. Test: sudo systemctl kill -s SIGKILL nginx met Restart=no tijdelijk, of eenvoudiger een wegwerp-unit:
sudo systemd-run --unit=crashtest -p OnFailure=notify-failed@crashtest.service /bin/false
# → push met de laatste 15 journal-regels van crashtest
Let op: OnFailure vuurt pas als de unit echt failed — dus na het bereiken van de start-limit, niet bij elke individuele crash die door Restart= wordt opgevangen. Dat is precies goed: je wilt geen push per herstart, wel één als herstarten niet meer helpt.
Stap 4: hangende services — de watchdog
Restart= reageert op een proces dat stopt. Een proces dat vastloopt in een deadlock, met alle threads bezig, stopt niet. Voor services die systemd's sd_notify ondersteunen (nginx niet; veel Go/Rust/Python-daemons wel, en alles wat via systemd-notify communiceert), is er WatchdogSec=:
[Service]
Type=notify
WatchdogSec=30s
# Als het proces niet elke 30 s WATCHDOG=1 stuurt, krijgt het SIGABRT en herstart systemd het
Restart=on-watchdog
Ondersteunt je service dat niet, dan bouw je de watchdog buiten de service: een timer die een echte request doet en bij falen herstart.
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
Twee regels bij zo'n externe watchdog: de healthcheck moet iets echts doen (een query, niet alleen "200 OK"), en de herstart moet gelogd én geteld worden — anders maskeer je een geheugenlek als "af en toe een herstart".
Stap 5: het cron-script dat de gaten dicht
Wat systemd zelf niet meldt: units die enabled-maar-inactive zijn, restart-loops die nét onder de start-limit blijven, en services die na een reboot niet terugkwamen.
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. Failed units
f=$(systemctl --failed --no-legend | awk '{print $1}' | paste -sd, -)
[ -n "$f" ] && MSG+=("failed: $f")
# 2. Enabled maar niet actief (templates, oneshots en units met niet-vervulde Condition uitgesloten)
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
# Netjes beëindigd (Result=success) = boot-job zoals cloud-init-main, geen storing.
# Een daemon die onverwacht met exit 0 stopt, vang je met Restart=always (stap 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. Restart-loops: NRestarts gestegen met ≥ 3 sinds vorige check (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
Valkuilen
Restart=alwaysop een oneshot. Een backup-script metRestart=alwaysdraait eindeloos opnieuw.Restart=is voor daemons; voor jobs gebruik je timers.ExecStartPredat faalt telt mee voor de start-limit. Eenmkdirdie faalt omdat een mount ontbreekt, brandt je tien pogingen op in seconden. Zet-voor het commando (ExecStartPre=-/bin/mkdir …) als falen acceptabel is.systemctl restartin een healthcheck zonder rate-limit. Als de database weg is, herstart je elke minuut een app die toch niet werkt, en spamt je de logs vol. Tel de herstarts en stop na drie: dan is het een alert, geen fix.KillMode=en kinderen. Een service die worker-processen spawnt (gunicorn, php-fpm): bijRestart=blijven oude workers soms hangen alsKillMode=process. Standaard (control-group) is bijna altijd juist.daemon-reloadvergeten. Een drop-in zondersystemctl daemon-reloadis een tekstbestand.systemctl showvertelt wat er echt actief is.
Wat je hiermee nog niet hebt
- Historie.
NRestartsreset bij elke boot. "Hoe vaak is deze service in de laatste 30 dagen herstart" vraagt een log die je zelf bijhoudt. - Fleet-zicht. Dertig servers, dertig
--failed-lijsten, dertig ntfy-topics. - Context. De service die om 03:12 crashte en de
apt-upgrade van 03:10 staan in twee logs; de correlatie is handwerk.
Zo doet monsys het
De monsys-agent leest de systemd-state van elke host: failed units, enabled-maar-inactive, NRestarts, restart-events met tijdstip, en de exit-status. Restart-loops en down-services worden gededupliceerde alerts met de journal-regels en de host-metrics van dat moment ernaast; de SLA-engine rekent per service een beschikbaarheidspercentage uit over 5-minuten-buckets, zodat "99,7 % vorige maand" een feit is en geen schatting. Herstarten blijft een handeling die een mens goedkeurt — via een ondertekende Emergency Action Token, met de output van systemctl restart in het audit-log.
FAQ
Herstart systemd een service automatisch na een crash?
Alleen als Restart= is ingesteld (on-failure of always); de standaard is no. En zelfs dan stopt systemd na StartLimitBurst pogingen binnen StartLimitIntervalSec en zet de unit op failed.
Hoe zie ik waarom een service failed is?
systemctl status <unit> toont de laatste regels; journalctl -u <unit> -b -p warning de volledige context sinds de boot. systemctl show <unit> -p Result -p ExecMainStatus geeft de exit-code en de reden (exit-code, signal, start-limit-hit, watchdog).
Werkt de watchdog met elke service?
Alleen met services die Type=notify gebruiken en periodiek WATCHDOG=1 sturen via sd_notify. Voor andere services bouw je een externe healthcheck met een timer, zoals in stap 4.
Geschreven door het monsys-team — sysadmins die dit dagelijks doen.
Zelf gedaan? Laat monsys het bijhouden.
Alles uit deze how-to draait in monsys als doorlopende check, met historie, alerts en audit-bewijs. 5 servers gratis, EU-gehost in België, geïnstalleerd in 60 seconden.