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.