Worum es geht
Ein Dashboard und ein Alarmsystem lösen zwei verschiedene Probleme. Ein Dashboard beantwortet die Frage "Wie sieht es gerade aus?", sobald du nachsiehst. Ein Alarmsystem beantwortet die Frage "Muss ich jetzt eingreifen?", und zwar ohne dass du dafür etwas tun musst. Für kleine Setups reicht oft ein Dashboard, solange du täglich hineinschaust. Sobald Dienste unbeaufsichtigt laufen, brauchst du zusätzlich eine Pipeline, die selbstständig prüft und dich anspricht.
Eine solche Pipeline besteht aus wenigen Bausteinen. Ein Collector fragt den Zustand ab, eine kleine SQLite-Datenbank merkt sich den letzten bekannten Zustand, eine Klassifikation entscheidet, ob eine Änderung relevant ist, und ein Benachrichtigungskanal sorgt dafür, dass du davon erfährst. Ein täglicher Heartbeat bestätigt, dass die Pipeline selbst noch läuft. Gesteuert wird das Ganze per cron oder systemd-Timer.
Anforderungen
Bevor du loslegst, klärst du ein paar Voraussetzungen. Das Skript braucht Zugriff auf den Docker-Daemon, in der Regel über den Unix-Socket. Lesezugriff genügt, denn du willst nur Zustände abfragen und keine Container steuern. Ein Socket-Proxy mit reinen Leserechten ist die sauberere Lösung, wenn das Skript in einem Container läuft.
Du brauchst außerdem einen Ort für den letzten bekannten Zustand. SQLite reicht dafür völlig aus: eine Datei, keine Zusatzdienste, einfach zu sichern. Für die Benachrichtigung wählst du einen Kanal, den du ohnehin ständig im Blick hast, etwa ntfy, Telegram oder E-Mail. Schließlich legst du den Takt fest. Für stabile Dienste genügt ein Check pro Stunde, für empfindliche Dienste sind fünf Minuten sinnvoll. Kürzere Intervalle bringen selten etwas, weil Docker Neustarts oft selbst abfängt.
Möglichkeiten im Vergleich
Du hast mehrere Wege, Container zu überwachen. Die Docker-eigene Restart-Policy startet abgestürzte Container neu, sagt dir aber nicht, dass etwas passiert ist. Ein Container, der alle zehn Sekunden abstürzt und neu startet, sieht im Dashboard oft trotzdem "running" aus.
Healthchecks im Container gehen einen Schritt weiter. Sie prüfen, ob der Dienst tatsächlich antwortet, und setzen den Status auf healthy oder unhealthy. Das ist die genauere Information, aber auch sie erzeugt allein noch keine Nachricht an dich.
Ein vollständiger Monitoring-Stack mit Prometheus und Alertmanager liefert Metriken und flexible Regeln. Er ist für kleine Setups oft mehr Betrieb, als der Nutzen rechtfertigt. Die eigene kleine Pipeline liegt dazwischen: Sie ist schnell gebaut, prüft genau das, was du brauchst, und bleibt für dich nachvollziehbar. Ihr Nachteil ist, dass du sie selbst pflegst.
Umsetzung
Das folgende Skript fragt alle Container ab, vergleicht ihren Zustand mit dem gespeicherten und meldet nur Änderungen. Es nutzt das Docker-SDK für Python (pip install docker) und urllib für die ntfy-Nachricht. Die Adresse des ntfy-Topics ist ein Beispielwert, den du durch deine eigene ersetzt.
import logging
import sqlite3
import urllib.request
from pathlib import Path
import docker
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("container_watch")
DB_PATH = Path("data/containers.db")
NTFY_URL = "https://ntfy.example.org/container-alerts" # Beispielwert
def init_db(conn: sqlite3.Connection) -> None:
conn.execute(
"CREATE TABLE IF NOT EXISTS state ("
"name TEXT PRIMARY KEY, status TEXT NOT NULL, health TEXT)"
)
def current_states(client: docker.DockerClient) -> dict[str, tuple[str, str]]:
states = {}
for c in client.containers.list(all=True):
health = c.attrs.get("State", {}).get("Health", {}).get("Status", "none")
states[c.name] = (c.status, health)
return states
def notify(message: str) -> None:
req = urllib.request.Request(NTFY_URL, data=message.encode("utf-8"), method="POST")
with urllib.request.urlopen(req, timeout=10):
pass
def run() -> None:
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(DB_PATH)
client = None
try:
init_db(conn)
client = docker.from_env()
known = {r[0]: (r[1], r[2]) for r in conn.execute("SELECT * FROM state")}
for name, (status, health) in current_states(client).items():
old = known.get(name)
if old is not None and old != (status, health):
log.warning("%s: %s/%s -> %s/%s", name, *old, status, health)
notify(f"{name}: {old[0]}/{old[1]} -> {status}/{health}")
conn.execute(
"INSERT INTO state (name, status, health) VALUES (?, ?, ?) "
"ON CONFLICT(name) DO UPDATE SET status=excluded.status, "
"health=excluded.health",
(name, status, health),
)
conn.commit()
except docker.errors.DockerException as exc:
log.error("Docker nicht erreichbar: %s", exc)
finally:
if client is not None:
client.close()
conn.close()
if __name__ == "__main__":
run()
Das Skript startest du per cron oder als systemd-Timer im gewählten Takt. Eine nicht erreichbare Docker-Schnittstelle sollte ebenfalls eine Nachricht auslösen, sonst schweigt die Pipeline genau dann, wenn etwas Grundlegendes kaputt ist. Ergänze dafür im except-Zweig einen notify-Aufruf mit einem eigenen Text. Ein täglicher Heartbeat, der eine Bestätigung "Pipeline läuft" sendet, deckt den Fall ab, dass das Skript selbst nicht mehr startet.
Für die Klassifikation kannst du später ein lokales Modell über Ollama oder eine Cloud-API einsetzen, etwa um Logzeilen eines abgestürzten Containers zusammenzufassen. Für die reine Zustandsüberwachung brauchst du das nicht. Eine feste Regel wie "running nach exited" ist verlässlicher als ein Modell und kostet nichts.
Typische Fallstricke
Der häufigste Fehler ist Alarm bei jedem Check. Wenn du bei jedem Lauf meldest, dass ein Container ausgefallen ist, stumpfst du dich schnell ab und ignorierst die Nachrichten irgendwann. Melde deshalb nur Zustandswechsel und, wenn nötig, eine Erinnerung nach einer längeren Frist.
Der zweite Stolperstein ist der Vergleich über die Container-ID. Nach einem Update mit neuem Image bekommt der Container eine neue ID, der Name bleibt aber gleich. Wer die ID als Schlüssel nutzt, verliert die Historie und meldet jeden aktualisierten Container als neu. Der Name oder ein stabiles Label ist der bessere Schlüssel, so wie im Beispiel.
Drittens ist running allein keine Gesundheitsauskunft. Ein Prozess kann laufen und trotzdem keine Anfragen beantworten. Definiere deshalb Healthchecks für wichtige Dienste und lies den Health-Status mit aus, wie es der Collector oben tut.
Viertens gehört das Skript selbst unter Beobachtung. Läuft der cron-Job nicht mehr, kommt keine Meldung, und genau das sieht aus wie "alles in Ordnung". Der tägliche Heartbeat schließt diese Lücke. Prüfe außerdem, dass die Systemzeit synchronisiert ist, sonst stimmen Zeitstempel in Logs und Nachrichten nicht mit anderen Diensten überein.
Wann sich das lohnt (und wann nicht)
Die eigene kleine Pipeline lohnt sich, wenn du einen oder wenige Docker-Hosts betreibst und Ausfälle bemerken willst, ohne einen ganzen Monitoring-Stack zu pflegen. Der Aufwand ist überschaubar: ein Skript, eine Datei als Datenbank, ein Benachrichtigungskanal. Du bestimmst selbst, was gemeldet wird, und kannst jede Regel im Code nachlesen.
Weniger sinnvoll ist sie, wenn du bereits ein Monitoring mit Alerting betreibst. Dann bindest du die Docker-Metriken dort ein, statt eine zweite Lösung zu pflegen. Auch bei sehr vielen Hosts skaliert ein Skript pro Rechner schlecht, dort sind zentrale Systeme mit Agenten die bessere Wahl.
Ein Dashboard bleibt trotzdem nützlich. Es zeigt dir den Überblick und erleichtert manuelle Eingriffe. Die Pipeline übernimmt den Teil, den ein Dashboard nicht leisten kann: dich zu erreichen, wenn du nicht hinsiehst.