
🌐 Auch auf: English · Français · Español
Ab 17 Uhr schaltet die Natur auf Dark Mode – systemweit, ohne Schalter, ohne Opt-out. Anders als hier im Blog lässt er sich nicht wegklicken. 🌑 Morgens einstellig, und an grauen Tagen reicht die PV-Anlage hier in Süddeutschland gerade noch für einen Kaffee – Espresso, nicht Cappuccino. ☕ Die Gedanken wandern zum letzten Mallorca-Urlaub: Sonne, Meer und die Playa de Palma. Zu Hause ist die Party leiser: Server statt Sangria, Dashboard statt Tanzfläche. 🍹
Ab hier wieder ernsthaft.
Server, und ganz besonders Webanwendungen, werden krank wie echte Patienten. Sie bekommen Fieber (CPU am Anschlag), Atemnot (volle Platte), Gedächtnislücken (Speicher läuft aus) oder einfach einen schlechten Tag, an dem sie langsamer antworten als sonst. Manche Symptome verschwinden von selbst, andere sind die ersten Anzeichen von etwas Ernstem, und man muss ihnen nachgehen. Genau das ist Monitoring: regelmäßig Puls, Temperatur und Blutdruck messen, Auffälligkeiten erkennen, bevor sie zum Notfall werden, und im Ernstfall wissen, wo man nachsehen muss.
Und wie beim Arzt gilt: Wer nur selbst den Puls fühlt, merkt den eigenen Kreislaufstillstand nicht. Genau diese Lücke hatte die Überwachung hier noch.
🩺 Der Check-up: wer prüft hier was?
Zur Einordnung: Hier gibt es zwei Welten. Im Internet stehen drei gemietete Server – der Blog und zwei Nextcloud-Instanzen. Zu Hause steht ein kleines Rack mit eigener Hardware, darunter der NAS- und Backup-Server, der ohnehin rund um die Uhr läuft und die meiste Zeit vor sich hin dümpelt. Über die Monate sind in beiden Welten Überwachungsschichten entstanden. Jede beantwortet eine andere Frage.
Auf den drei Servern im Internet – jeder überwacht sich selbst:
| Schicht | Wie oft | Was sie merkt |
|---|---|---|
| Monit | alle 2 min | Dienst gestoppt, fail2ban-Jail weg, Sperren und Firewall uneins, Pod hängt, Zertifikat < 21 Tage, Backup zu alt, Mail-Zustellfehler |
| Prometheus + Grafana | laufend | Platte, RAM, CPU, Container am Limit, Neustart-Schleifen, OOM-Kills, PHP-FPM ausgelastet |
| Lebenszeichen | laufend | ein nächtlicher Job (AIDE, rkhunter, ClamAV, Updates …) hat sich nicht gemeldet |
| Loki | laufend | SELinux-Sperren, fail2ban-Fehler, OOM im Kernel, ein Dienst schreibt zu viele Fehler |
| Wirkungstests | bei jedem Playbook-Lauf | Sperrkette, Filter, SELinux-Domänen, Messwerte vorhanden |
Vom NAS-Server zu Hause – der Blick von außen und auf die eigene Hardware:
| Schicht | Wie oft | Schaut auf | Was sie merkt |
|---|---|---|---|
| Angriffs-Auswertung | täglich | die Server im Internet | POST-Wellen auf PHP-Einstiegspunkte, PHP-FPM-Abstürze |
| Außensicht | wöchentlich | die Server im Internet | offene Ports, Zertifikate, Versionen, vergessene alte Server |
| Temperaturen | alle 10 min | nur das Rack zu Hause | Wärmeverlauf der eigenen Hardware; echte Überhitzung meldet das iLO selbst per Mail. Die Server im Internet stehen im Rechenzentrum – deren Temperatur ist Sache des Hosters. |
Was man mitnimmt: Jede Schicht ist für eine bestimmte Art von Ausfall da. Monit sieht, ob etwas läuft, Prometheus, ob es reicht, Loki, warum es klemmt, die Lebenszeichen, ob überhaupt etwas passiert ist.
🚑 Die Lücke: wer ruft den Arzt, wenn der Patient umfällt?
Schaut man auf die erste Tabelle, fällt es auf: Alles dort läuft auf dem Server, den es überwacht. Stürzt ein Server komplett ab, hat der Hoster eine Störung oder ist das Netz weg, gehen Monit, Grafana und Loki mit unter. Niemand verschickt eine Mail – und Stille sieht im Postfach genauso aus wie „alles gut“. Ein unüberwachter Server ist Schrödingers Server: zugleich up und down – bis jemand die Kiste öffnet, meistens ein Nutzer mit dem Telefon in der Hand. 🐈📦
Bemerkt hätte man es bisher erst über die wöchentliche Testmail, die ausbleibt, oder beim Montags-Check von außen. Im schlimmsten Fall also nach Tagen. Dazu kommt ein zweiter Fall: Alle Dienste laufen, aber die Seite zeigt trotzdem nur „Error establishing a database connection“ oder die Nextcloud hängt im Wartungsmodus. Für Monit ist dann alles grün. Die Frage ist übrigens fast zweitausend Jahre alt – Juvenal stellte sie in seinen Satiren: Quis custodiet ipsos custodes? Wer bewacht die Wächter?
Dabei geht es um mehr als Technik: Ich möchte einen Ausfall selbst bemerken, bevor sich jemand bei mir meldet. Das ist der Unterschied zwischen reaktiv und proaktiv. Reaktiv heißt: Man handelt, nachdem ein Problem aufgefallen ist – meist, weil jemand anruft oder schreibt „die Cloud geht nicht“. Dann ist der Schaden schon da, und der Nutzer hat ihn vor mir gesehen. Proaktiv heißt: Man sorgt vorher dafür, dass man Probleme selbst und früh erkennt – im besten Fall, bevor sie jemand merkt, mindestens aber so, dass man auf die erste Nachricht antworten kann: „Ist bekannt, ich bin dran.“ Jede Schicht in den Tabellen oben ist ein Stück Proaktivität; ohne Blick von außen blieb ausgerechnet der größte Ausfall reaktiv. Es gibt zwei Arten von Admins: die, die von einem Ausfall aus ihrem Monitoring erfahren – und die, die es von ihren Nutzern erfahren.
Was man mitnimmt: Eine Überwachung, die auf demselben Rechner läuft wie der Patient, kann seinen Totalausfall nicht melden. Dafür braucht es jemanden von außen.
🏠 Der Hausbesuch: ein Erreichbarkeits-Check von zu Hause
Die Lösung ist bewusst klein und braucht keine neue Hardware – der NAS-Server zu Hause ist ohnehin wach. Ein Python-Skript ruft dort alle 5 Minuten jede der drei Seiten im Internet so auf wie ein Besucher, nur ohne Browser:
- WordPress: Die Startseite antwortet mit 200 und ist eine echte Seite, keine Datenbank-Fehlerseite und kein Wartungshinweis.
- Nextcloud:
status.phpmeldet „installiert“, „nicht im Wartungsmodus“ und „kein Datenbank-Upgrade offen“. - Collabora:
/hosting/discoveryliefert das WOPI-XML, das Nextcloud zum Öffnen von Dokumenten braucht. - Zertifikat: einmal am Tag – ungültig oder weniger als 14 Tage gültig ist eine Meldung.
Gemailt wird nur bei einem Wechsel: „Ausfall“ nach zwei fehlgeschlagenen Läufen hintereinander (ein einzelner Aussetzer ist kein Alarm), „wieder da“ mit der Dauer des Ausfalls, und eine Erinnerung alle 12 Stunden, solange etwas down bleibt. Fallen alle Ziele gleichzeitig aus, prüft das Skript erst zwei neutrale Seiten im Internet – sind auch die weg, liegt es an der eigenen Leitung, und es gibt keinen Fehlalarm. Im Kleinen ist das das Zwei-Armeen-Problem der Informatik: Bleibt die Antwort aus, weiß man nicht, ob der Empfänger gefallen ist oder der Bote – also fragt man einen Dritten.
Der User-Agent enthält „uptime“, damit die Besucherstatistik die 288 Aufrufe am Tag aus dem eigenen Wohnzimmer nicht mitzählt. Und ein Gruß an Heisenberg: Eine Messung soll das Gemessene möglichst nicht verändern – zwölf Aufrufe pro Stunde gehen im Grundrauschen unter.
Die eigene Grenze: Ist der NAS-Server aus oder die Leitung zu Hause weg, prüft niemand. Trotzdem ein großer Schritt – vorher prüfte von außen nur ein Check pro Woche.
Was man mitnimmt: Prüfe von außen, was der Nutzer sieht – nicht, ob ein Prozess läuft. Und melde Zustandswechsel, nicht Zustände; sonst landen die Mails bald ungelesen im Archiv. Ein Alarm, der ständig klingelt, ist ein Rauchmelder in der Sauna. 🔥
💓 Blutdruck messen: was „Pressure“ aussagt
Neu im Dashboard ist auch ein Wert, der lange leer blieb: Pressure (Pressure Stall Information, PSI). Er misst nicht, wie voll etwas ist, sondern wie lange Programme warten mussten, weil eine Ressource knapp war. Eine CPU kann zu 100 % beschäftigt sein, ohne dass jemand wartet – erst das Warten macht einen Server spürbar langsam. An der Supermarktkasse interessiert schließlich auch niemanden, wie ausgelastet die Kassiererin ist – sondern wie lang die Schlange ist.
$ cat /proc/pressure/memory
some avg10=0.00 avg60=0.00 avg300=0.00 total=31
full avg10=0.00 avg60=0.00 avg300=0.00 total=31- cpu / memory / io: gewartet auf Rechenzeit, auf freien Speicher (der Kernel musste erst aufräumen – das früheste Zeichen vor OOM-Kills) oder auf die Platte.
- some: Anteil der Zeit, in dem mindestens ein Prozess gewartet hat.
- full: Anteil der Zeit, in dem alle gewartet haben und nichts voranging – der ernstere Wert.
- avg10 / avg60 / avg300: Prozent der Zeit, gemittelt über 10 Sekunden, 1 und 5 Minuten.
Faustregel: einzelne Prozent sind normal. some dauerhaft über 10–20 % ist spürbar, full bei Speicher oder Platte über längere Zeit deutlich über 0 ein Befund.
Ein eigener Fehler am Rande: Auf diesen Servern ist PSI im Kernel standardmäßig aus und wird mit dem Boot-Parameter psi=1 eingeschaltet. Gesetzt war er schnell – aktiv wird er aber erst beim nächsten Start. Auf dem Blog kam der in der folgenden Nacht, die beiden Nextcloud-Server liefen noch anderthalb Tage mit leerem Panel, bis sie von Hand neu gestartet wurden.
Was man mitnimmt: Ein Boot-Parameter gilt erst nach dem Boot. Eine Änderung ist erst fertig, wenn der Wert dort ankommt, wo man ihn sehen will.
🔍 Ein Befund beim Check-up
Beim Durchsehen fiel noch etwas auf: Das Temperatur-Skript für das Rack zu Hause – es misst nur die eigene Hardware, nicht die Server im Internet – hat keinen Mailversand eingerichtet. Die erste Sorge war, dass eine Überhitzung also niemand melden würde. Ein Blick ins iLO zeigte: Dessen eigene AlertMail ist längst eingerichtet und meldet kritische Temperaturen selbst. Das Skript liefert nur die frühen, leisen Hinweise im Bericht.
Was man mitnimmt: Bevor man einen neuen Alarm baut, nachsehen, was schon alarmiert. Doppelte Alarme sind kein Gewinn.
🔭 Wie es weitergeht: schärfer statt mehr
Fertig ist so eine Überwachung nie. Weiterentwickeln heißt hier aber nicht, neue Werkzeuge dazuzustellen – sondern die vorhandenen Tests schärfer und spezifischer zu machen. Ein paar Überlegungen, die realistisch sind und ohne neue Infrastruktur auskommen – Ideen, keine Versprechen:
- Funktion statt Erreichbarkeit: Ein Testkonto meldet sich an der Nextcloud an, lädt per WebDAV eine kleine Datei hoch, holt sie wieder ab und vergleicht die Prüfsumme. Das prüft Anmeldung, Speicher, Datenbank und Redis in einem Zug.
- Inhalt statt Statuscode: Auf dem Blog öffnet ein Test einen bestimmten Beitrag und erwartet dessen Überschrift – eine Seite kann mit 200 antworten und trotzdem leer oder aus einem alten Cache sein.
- Langsam vor kaputt: Die Antwortzeit mitschreiben und warnen, wenn sie deutlich über dem eigenen Median der letzten Tage liegt. Die meisten Ausfälle kündigen sich als „zäh“ an.
- Gegenseitige Kontrolle: Die drei Server im Internet prüfen sich gegenseitig – dann bleibt auch dann jemand wach, wenn die Leitung zu Hause ausfällt.
- Wer prüft den Hausbesuch? Der Check selbst braucht ein Lebenszeichen, etwa einen Zeitstempel, den eine Überwachung im Internet vermisst, wenn er ausbleibt. Juvenal lässt grüßen.
Dazu kommt das Feintuning: Schwellen, die zu oft oder zu selten auslösen, werden nachgezogen, und jeder echte Vorfall, den kein Test bemerkt hat, wird zur Frage: Welcher Test hätte ihn gefunden?
Was man mitnimmt: Eine Überwachung reift mit jedem Vorfall. Je spezifischer ein Test, desto näher ist er an dem, was ein Nutzer wirklich tut.
📜 Das Skript
Nur die Standardbibliothek von Python, Mail über den lokalen mail-Befehl (hier msmtp). Die Konfiguration mit Adressen und Mail-Empfänger liegt getrennt daneben, unten mit Beispielwerten.
#!/usr/bin/env python3
"""
uptime_check.py - is every site still reachable and working, seen from outside?
Monit, Prometheus, Grafana and Loki run on the servers they watch. If a whole
server goes down (crash, hoster outage, network), they go down with it and
nobody sends a mail. This script runs on a machine at home every 5 minutes,
checks each site the way a visitor would and mails only when the state
changes: DOWN after <fail_after> failed runs in a row, UP again with the
downtime, a reminder every <remind_hours> while it stays down.
uptime_check.py run all checks (cron)
uptime_check.py --dry-run run all checks, print mails instead of sending
uptime_check.py --status show the stored state
Kinds of checks (uptime_check.conf, [targets]):
wordpress page answers 200 and is a real page, not a database error
nextcloud status.php: installed, not in maintenance, no pending DB upgrade
collabora /hosting/discovery answers with the WOPI discovery XML
Every target also gets a TLS certificate check (warning below cert_warn_days).
Standard library only. Mail goes through the local "mail" command (msmtp).
"""
import configparser, datetime as dt, json, os, socket, ssl, subprocess, sys, time
import urllib.error, urllib.request
from urllib.parse import urlsplit
HERE = os.path.dirname(os.path.abspath(__file__))
CONF = os.path.join(HERE, "uptime_check.conf")
STATE = os.path.join(HERE, "state.json")
LOG = os.path.join(HERE, "uptime_check.log")
UA = "uptime-check/1.0 (+homelab)" # "uptime" keeps it out of the visitor statistics
def log(msg):
with open(LOG, "a") as f:
f.write(f"{dt.datetime.now():%Y-%m-%d %H:%M:%S} {msg}\n")
def fetch(url, timeout):
"""GET url -> (status, body text, seconds). Raises on network errors."""
req = urllib.request.Request(url, headers={"User-Agent": UA})
t0 = time.monotonic()
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
body = r.read(2_000_000).decode("utf-8", "replace")
return r.status, body, time.monotonic() - t0
except urllib.error.HTTPError as e:
return e.code, "", time.monotonic() - t0
# Each check returns None when everything is fine, otherwise a short reason.
def check_wordpress(base, timeout):
status, body, secs = fetch(base, timeout)
if status != 200:
return f"HTTP {status}"
if "Error establishing a database connection" in body:
return "WordPress meldet: keine Datenbankverbindung"
if "Briefly unavailable for scheduled maintenance" in body:
return "WordPress steckt im Wartungsmodus"
if "wp-content" not in body or "</html>" not in body:
return "Antwort ist keine vollständige WordPress-Seite"
return None
def check_nextcloud(base, timeout):
status, body, secs = fetch(base.rstrip("/") + "/status.php", timeout)
if status != 200:
return f"status.php: HTTP {status}"
try:
s = json.loads(body)
except ValueError:
return "status.php liefert kein JSON"
if not s.get("installed"):
return "status.php: installed = false"
if s.get("maintenance"):
return "Nextcloud ist im Wartungsmodus"
if s.get("needsDbUpgrade"):
return "Nextcloud wartet auf ein Datenbank-Upgrade (occ upgrade)"
return None
def check_collabora(base, timeout):
status, body, secs = fetch(base.rstrip("/") + "/hosting/discovery", timeout)
if status != 200:
return f"discovery: HTTP {status}"
if "<wopi-discovery" not in body:
return "discovery liefert kein WOPI-XML"
return None
CHECKS = {"wordpress": check_wordpress, "nextcloud": check_nextcloud, "collabora": check_collabora}
def cert_days_left(base, timeout):
host = urlsplit(base).hostname
ctx = ssl.create_default_context() # also fails on an invalid or wrong-name certificate
with socket.create_connection((host, 443), timeout=timeout) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
not_after = ssl.cert_time_to_seconds(tls.getpeercert()["notAfter"])
return int((not_after - time.time()) // 86400)
def run_check(kind, base, timeout):
try:
return CHECKS[kind](base, timeout)
except (urllib.error.URLError, socket.timeout, TimeoutError, ConnectionError, ssl.SSLError, OSError) as e:
reason = getattr(e, "reason", e)
return f"nicht erreichbar ({reason})"
def online(refs, timeout):
"""Is this machine itself online? True if any reference URL answers at all."""
for url in refs:
try:
fetch(url, timeout)
return True
except Exception:
continue
return False
def fmt_duration(seconds):
m = int(seconds // 60)
return f"{m // 60} h {m % 60} min" if m >= 60 else f"{m} min"
def send_mail(to, subject, text, dry_run):
if dry_run:
print(f"--- MAIL an {to}: {subject}\n{text}\n---")
return
r = subprocess.run(["mail", "-s", subject, to], input=text, text=True, capture_output=True)
if r.returncode != 0:
log(f"MAIL FEHLER rc={r.returncode}: {r.stderr.strip()}")
def main():
dry_run = "--dry-run" in sys.argv
cfg = configparser.ConfigParser()
if not cfg.read(CONF):
sys.exit(f"config missing: {CONF}")
g = cfg["general"]
timeout = g.getint("timeout", 20)
fail_after = g.getint("fail_after", 2)
remind = g.getint("remind_hours", 12) * 3600
cert_warn = g.getint("cert_warn_days", 14)
refs = g.get("reference_urls", "").split()
mail_to = g["mail_to"]
try:
state = json.load(open(STATE))
except (OSError, ValueError):
state = {}
if "--status" in sys.argv:
print(json.dumps(state, indent=2, ensure_ascii=False))
return
now = int(time.time())
targets = {name: val.split(None, 1) for name, val in cfg["targets"].items()}
results = {name: run_check(kind, url, timeout) for name, (kind, url) in targets.items()}
# Everything failed at once? Then it is more likely this machine's own
# line than all servers - check a neutral reference before alarming.
if all(results.values()) and not online(refs, timeout):
log("alle Ziele fehlgeschlagen und Referenz nicht erreichbar - eigene Leitung offline, nichts gezählt")
return
mails = []
for name, (kind, url) in targets.items():
st = state.setdefault(name, {"status": "up", "fails": 0, "since": now, "last_mail": 0})
reason = results[name]
if reason is None:
if st["status"] == "down":
mails.append(f"WIEDER DA {name} ({url}) nach {fmt_duration(now - st['since'])}")
log(f"UP {name} nach {fmt_duration(now - st['since'])}")
st.update(status="up", since=now)
st["fails"] = 0
else:
st["fails"] += 1
log(f"FAIL {name} ({st['fails']}x): {reason}")
if st["status"] == "up" and st["fails"] >= fail_after:
st.update(status="down", since=now, last_mail=now, reason=reason)
mails.append(f"AUSFALL {name} ({url}): {reason}")
elif st["status"] == "down" and now - st["last_mail"] >= remind:
st["last_mail"] = now
mails.append(f"NOCH DOWN {name} ({url}) seit {fmt_duration(now - st['since'])}: {reason}")
# Certificate: checked once per target and day, so at most one warning a day.
if reason is None and now - st.get("cert_checked", 0) >= 86400:
st["cert_checked"] = now
try:
days = cert_days_left(url, timeout)
if days < cert_warn:
mails.append(f"ZERTIFIKAT {name}: läuft in {days} Tagen ab (Erneuerung hängt?)")
except Exception as e:
mails.append(f"ZERTIFIKAT {name}: Prüfung fehlgeschlagen ({e})")
if mails:
down = sum(1 for s in state.values() if s["status"] == "down")
subject = f"Erreichbarkeit: {down} Ziel(e) down" if down else "Erreichbarkeit: alles wieder erreichbar"
text = "\n".join(mails) + f"\n\nGeprüft {dt.datetime.now():%Y-%m-%d %H:%M} von {socket.gethostname()}.\n"
send_mail(mail_to, subject, text, dry_run)
if not dry_run:
tmp = STATE + ".tmp"
with open(tmp, "w") as f:
json.dump(state, f, indent=1)
os.replace(tmp, STATE)
if __name__ == "__main__":
main()
Die Konfiguration uptime_check.conf (Beispielwerte):
# uptime_check.conf - chmod 600 (contains the mail address)
[general]
mail_to = you@example.org
# seconds per request
timeout = 20
# failed runs in a row before a DOWN mail (cron every 5 min -> 10 min)
fail_after = 2
remind_hours = 12
cert_warn_days = 14
# neutral sites: if ALL targets fail, these tell "server down" from "own line down"
reference_urls = https://www.google.com/generate_204 https://one.one.one.one/
[targets]
# name = kind base-url
blog = wordpress https://blog.example.org/
cloud = nextcloud https://cloud.example.org/
office = collabora https://office.example.org/
Und der Eintrag in der crontab:
*/5 * * * * cd $HOME/uptime-check && python3 -I uptime_check.py >> cron.log 2>&1Zum Ausprobieren: --dry-run zeigt die Mails nur an, --status den gespeicherten Zustand. Einen Ausfall testet man am einfachsten mit einem zusätzlichen Ziel auf eine Adresse, die es nicht gibt (.invalid) – nicht mit falschen Pfaden auf dem eigenen Server, sonst sperrt fail2ban den eigenen Anschluss.
🎯 Die Lehren
- Überwachung auf dem Server selbst kann dessen Totalausfall nicht melden – einer muss von außen schauen.
- Prüfe, was der Nutzer sieht: eine echte Seite, ein „installed: true“ – nicht nur einen laufenden Prozess.
- Melde Zustandswechsel, nicht Zustände; zwei Fehlschläge vor dem Alarm filtern Aussetzer.
- Pressure zeigt nicht Auslastung, sondern Warten – das, was Nutzer als „langsam“ erleben.
- Ein Boot-Parameter wirkt erst nach dem Boot; fertig ist es erst, wenn der Wert ankommt.
Und jetzt: Heizung an, Tee aufsetzen, die Mallorca-Fotos noch einmal durchklicken. Die Server melden sich, wenn der Arzt gebraucht wird. There’s no place like 127.0.0.1 – aber genau 127.0.0.1 kann niemandem sagen, dass es selbst down ist. Dafür gibt es jetzt den Hausbesuch. 😉





