Einen selbstheilenden VPS mit Prometheus und Ollama aufbauen
Verbinden Sie Prometheus-Alerts mit einem lokalen LLM, das Fehler diagnostiziert und sichere Gegenmaßnahmen auf Ihrem VPS ausführt. Vollständiger Arbeitscode mit Allowlists, Dry-Run-Modus und menschlichen Freigabekontrollen.
Ihr Server fällt um 3 Uhr morgens aus. Sie werden geweckt, verbinden sich halb schlafend per SSH, starten den abgestürzten Dienst neu und gehen wieder ins Bett. Nach dem dritten Mal fragen Sie sich: Könnte der Server sich nicht einfach selbst reparieren?
Kann er. Dieses Tutorial baut eine selbstheilende Rückkopplungsschleife auf einem einzelnen VPS auf. Prometheus und node_exporter sammeln Metriken. Alertmanager feuert, wenn Schwellenwerte überschritten werden. Ein Python-Webhook-Empfänger fängt diese Alerts ab und übergibt den Kontext an Ollama. Das LLM diagnostiziert das Problem und empfiehlt eine Gegenmaßnahme. Steht die Maßnahme auf der Allowlist, wird sie automatisch ausgeführt. Ist sie destruktiv, muss ein Mensch sie zuerst über Discord freigeben.
Der vollständige Ablauf sieht so aus:
node_exporter -> Prometheus -> Alertmanager -> webhook receiver -> Ollama -> remediation engine -> action
|
audit log + Discord
Jede Aktion wird protokolliert. Jede LLM-Empfehlung wird als nicht vertrauenswürdige Eingabe behandelt. Das ist der Teil, den die meisten AIOps-Inhalte überspringen, und der Teil, der am meisten zählt.
Was ist ein selbstheilender Server und warum sollte man einen auf einem VPS bauen?
Ein selbstheilender Server erkennt Ausfälle anhand von Metriken und Alert-Regeln und löst dann Gegenmaßnahmen ohne menschliches Eingreifen aus. In Kombination mit einem lokalen LLM wie Ollama diagnostiziert das System Ursachen aus dem Alert-Kontext und führt Aktionen von der Allowlist aus: einen abgestürzten Dienst neustarten, Speicherplatz freigeben oder einen außer Kontrolle geratenen Prozess beenden.
Enterprise-Teams verwenden dafür PagerDuty, Rundeck oder StackStorm. Diese Tools setzen ein Team, eine Serverflotte und ein Budget voraus. Auf einem einzelnen VPS brauchen Sie etwas Leichteres. Das hier beschriebene „Sentinel Pattern" ist ein eigenständiger Agent, der Ihren Server überwacht und häufige Probleme automatisch behebt, mit Sicherheitskontrollen, die verhindern, dass das LLM etwas Gefährliches tut.
Voraussetzungen
- Ein VPS mit 4+ vCPU und 8 GB RAM (das LLM braucht Platz neben Ihren Diensten)
- Debian 12 oder Ubuntu 24.04
- Docker und Docker Compose installiert
- Ollama installiert mit mindestens einem heruntergeladenen Modell (
qwen2.5:7bempfohlen für strukturierte JSON-Ausgabe) - Ein Nicht-Root-Benutzer mit sudo-Zugang
- Eine Discord-Webhook-URL (für Freigabebenachrichtigungen)
Wie richten Sie Prometheus, node_exporter und Alertmanager mit Docker Compose ein?
Stellen Sie den Monitoring-Stack als drei Container bereit: Prometheus v3.10.0 sammelt Metriken, node_exporter v1.10.2 stellt Host-Metriken bereit, und Alertmanager v0.31.1 leitet Alerts an Ihren Webhook-Empfänger weiter. Der gesamte Stack startet mit einem einzigen docker compose up -d.
Erstellen Sie das Projektverzeichnis:
mkdir -p ~/sentinel/{prometheus,alertmanager}
cd ~/sentinel
Docker Compose Stack
Erstellen Sie docker-compose.yml:
services:
prometheus:
image: prom/prometheus:v3.10.0
container_name: prometheus
restart: unless-stopped
user: "65534:65534"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./prometheus/alert_rules.yml:/etc/prometheus/alert_rules.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=7d'
- '--web.enable-lifecycle'
ports:
- "127.0.0.1:9090:9090"
networks:
- sentinel
node-exporter:
image: prom/node-exporter:v1.10.2
container_name: node-exporter
restart: unless-stopped
pid: host
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
- '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)'
networks:
- sentinel
alertmanager:
image: prom/alertmanager:v0.31.1
container_name: alertmanager
restart: unless-stopped
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports:
- "127.0.0.1:9093:9093"
extra_hosts:
- "host.docker.internal:host-gateway"
networks:
- sentinel
volumes:
prometheus_data:
networks:
sentinel:
driver: bridge
Prometheus und Alertmanager binden nur an 127.0.0.1. Monitoring-Dashboards dem Internet auszusetzen ist ein häufiger Konfigurationsfehler, der interne Metriken für jeden sichtbar macht, der Ihre IP scannt.
Prometheus-Konfiguration
Erstellen Sie prometheus/prometheus.yml:
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets:
- alertmanager:9093
scrape_configs:
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
Alertmanager-Konfiguration
Erstellen Sie alertmanager/alertmanager.yml:
global:
resolve_timeout: 5m
route:
receiver: sentinel-webhook
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: sentinel-webhook
webhook_configs:
- url: 'http://host.docker.internal:5001/alert'
send_resolved: true
max_alerts: 10
Die Webhook-URL zeigt auf den Python-Empfänger, der auf dem Host läuft. host.docker.internal löst zur Host-IP innerhalb von Docker-Containern auf. Die extra_hosts-Direktive in der obigen Compose-Datei bildet sie auf das Host-Gateway unter Linux ab.
Setzen Sie die Dateiberechtigungen, bevor Sie den Stack starten:
chmod 644 prometheus/prometheus.yml prometheus/alert_rules.yml alertmanager/alertmanager.yml
Welche Alert-Regeln sollten Sie für häufige VPS-Ausfälle konfigurieren?
Vier Alert-Regeln decken die häufigsten VPS-Ausfälle ab: Festplatte füllt sich, Speichererschöpfung, ein Dienst fällt aus und anhaltend hohe CPU-Auslastung. Jede Regel feuert, wenn ein Schwellenwert für eine definierte Dauer überschritten wird, damit Prometheus Zeit hat, vorübergehende Spitzen herauszufiltern.
Erstellen Sie prometheus/alert_rules.yml:
groups:
- name: vps_health
rules:
- alert: DiskSpaceLow
expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 > 85
for: 5m
labels:
severity: warning
annotations:
summary: "Disk usage above 85%"
description: "Root filesystem is {{ $value | printf \"%.1f\" }}% full."
remediation_hint: "prune_docker_images"
- alert: MemoryHigh
expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 90
for: 5m
labels:
severity: critical
annotations:
summary: "Memory usage above 90%"
description: "Available memory is {{ $value | printf \"%.1f\" }}% used."
remediation_hint: "kill_top_memory_process"
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Target {{ $labels.job }} is down"
description: "Scrape target {{ $labels.instance }} has been unreachable for over 1 minute."
remediation_hint: "restart_service"
- alert: HighCPU
expr: 100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 85
for: 10m
labels:
severity: warning
annotations:
summary: "CPU usage above 85% for 10 minutes"
description: "Average CPU usage is {{ $value | printf \"%.1f\" }}%."
remediation_hint: "identify_cpu_hog"
| Alert | PromQL-Schwellenwert | Dauer | Schweregrad | Standard-Gegenmaßnahme |
|---|---|---|---|---|
| DiskSpaceLow | Root-FS > 85 % voll | 5 Min | warning | Docker-Images bereinigen |
| MemoryHigh | Verfügbarer RAM < 10 % | 5 Min | critical | Speicherintensivsten Prozess beenden |
| ServiceDown | Scrape-Ziel unerreichbar | 1 Min | critical | Dienst neustarten |
| HighCPU | Durchschn. CPU > 85 % | 10 Min | warning | CPU-Verbraucher identifizieren |
Die Annotation remediation_hint ist ein benutzerdefiniertes Feld. Sie teilt der Remediation Engine mit, welche Aktion vorgeschlagen werden soll, wenn die LLM-Diagnose uneindeutig ist.
Starten Sie den Stack:
cd ~/sentinel
docker compose up -d
[+] Running 4/4
✔ Network sentinel_sentinel Created
✔ Container node-exporter Started
✔ Container prometheus Started
✔ Container alertmanager Started
Prüfen Sie, ob Prometheus Ziele scrapt:
curl -s http://127.0.0.1:9090/api/v1/targets | python3 -m json.tool | grep -A2 '"health"'
"health": "up",
"lastScrape": "2026-03-19T14:30:15.123Z",
Beide Ziele sollten "health": "up" anzeigen.
Wie fängt der Webhook-Empfänger Alertmanager-Alerts ab?
Der Webhook-Empfänger ist eine Flask-Anwendung, die auf POST-Requests von Alertmanager lauscht, den Alert-Kontext extrahiert, Ollama für eine Diagnose abfragt und das Ergebnis an die Remediation Engine übergibt. Er läuft auf dem Host, nicht in einem Container, weil er Zugriff auf Docker und systemd benötigt, um Gegenmaßnahmen auszuführen.
Installieren Sie die Abhängigkeiten:
sudo apt install -y python3-pip python3-venv jq
mkdir -p ~/sentinel/receiver
cd ~/sentinel/receiver
python3 -m venv venv
source venv/bin/activate
pip install flask requests pydantic
Erstellen Sie ~/sentinel/receiver/sentinel.py:
#!/usr/bin/env python3
"""Sentinel: self-healing webhook receiver for Alertmanager."""
import json
import logging
import os
import subprocess
import time
from datetime import datetime, timezone
from pathlib import Path
import requests
from flask import Flask, request, jsonify
from pydantic import BaseModel, field_validator
app = Flask(__name__)
# --- Configuration ---
OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen2.5:7b")
DISCORD_WEBHOOK = os.environ.get("DISCORD_WEBHOOK", "")
DRY_RUN = os.environ.get("SENTINEL_DRY_RUN", "false").lower() == "true"
AUDIT_LOG = Path(os.environ.get("SENTINEL_AUDIT_LOG", "/var/log/sentinel/audit.jsonl"))
AUDIT_LOG.parent.mkdir(parents=True, exist_ok=True)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s"
)
logger = logging.getLogger("sentinel")
# --- Safety: Action Allowlist ---
ALLOWED_ACTIONS = {
"restart_service": {
"command": "docker restart {service}",
"requires_approval": False,
"allowed_services": ["nginx", "docker", "prometheus", "node-exporter", "alertmanager"],
},
"prune_docker_images": {
"command": "docker image prune -af --filter 'until=72h'",
"requires_approval": False,
},
"kill_top_memory_process": {
"command": "ps aux --sort=-%mem | awk 'NR==2{print $2}' | xargs kill -15",
"requires_approval": True,
},
"identify_cpu_hog": {
"command": "ps aux --sort=-%cpu | head -5",
"requires_approval": False,
"read_only": True,
},
"clear_journal_logs": {
"command": "journalctl --vacuum-size=200M",
"requires_approval": False,
},
"add_swap": {
"command": "fallocate -l 2G /swapfile && chmod 600 /swapfile && mkswap /swapfile && swapon /swapfile",
"requires_approval": True,
},
}
class Diagnosis(BaseModel):
"""Structured LLM diagnosis output."""
severity: str
root_cause: str
recommended_action: str
reasoning: str
@field_validator("recommended_action")
@classmethod
def action_must_be_allowed(cls, v):
if v not in ALLOWED_ACTIONS:
raise ValueError(f"Action '{v}' is not in the allowlist")
return v
# --- Ollama Integration ---
def query_ollama(alert_data: dict) -> Diagnosis | None:
"""Send alert context to Ollama and parse structured diagnosis."""
prompt = f"""You are a server diagnostics agent. Analyze this alert and respond with a JSON diagnosis.
Alert: {alert_data.get('labels', {}).get('alertname', 'unknown')}
Status: {alert_data.get('status', 'unknown')}
Severity: {alert_data.get('labels', {}).get('severity', 'unknown')}
Summary: {alert_data.get('annotations', {}).get('summary', '')}
Description: {alert_data.get('annotations', {}).get('description', '')}
Remediation hint: {alert_data.get('annotations', {}).get('remediation_hint', 'none')}
Instance: {alert_data.get('labels', {}).get('instance', 'unknown')}
Started at: {alert_data.get('startsAt', 'unknown')}
Available actions: {', '.join(ALLOWED_ACTIONS.keys())}
Respond ONLY with a JSON object. Fields:
- severity: "low", "medium", "high", or "critical"
- root_cause: one-sentence explanation of the likely cause
- recommended_action: exactly one action from the available actions list
- reasoning: why you chose this action"""
schema = Diagnosis.model_json_schema()
try:
start = time.monotonic()
resp = requests.post(
f"{OLLAMA_URL}/api/chat",
json={
"model": OLLAMA_MODEL,
"messages": [{"role": "user", "content": prompt}],
"format": schema,
"stream": False,
"options": {"temperature": 0},
},
timeout=120,
)
resp.raise_for_status()
elapsed = time.monotonic() - start
logger.info(f"Ollama responded in {elapsed:.1f}s")
content = resp.json()["message"]["content"]
diagnosis = Diagnosis.model_validate_json(content)
return diagnosis
except Exception as e:
logger.error(f"Ollama query failed: {e}")
return None
# --- Remediation Engine ---
def execute_action(action_name: str, context: dict) -> dict:
"""Execute an allowlisted remediation action."""
action = ALLOWED_ACTIONS.get(action_name)
if not action:
return {"status": "blocked", "reason": f"Action '{action_name}' not in allowlist"}
command = action["command"]
# Template the service name if needed
if "{service}" in command:
service = context.get("service", "")
if service not in action.get("allowed_services", []):
return {"status": "blocked", "reason": f"Service '{service}' not in allowed_services"}
command = command.format(service=service)
if DRY_RUN:
logger.info(f"DRY RUN: would execute: {command}")
return {"status": "dry_run", "command": command}
if action.get("requires_approval"):
approved = request_discord_approval(action_name, command, context)
if not approved:
return {"status": "awaiting_approval", "command": command}
try:
result = subprocess.run(
command,
shell=True,
capture_output=True,
text=True,
timeout=60,
)
return {
"status": "executed",
"command": command,
"returncode": result.returncode,
"stdout": result.stdout[:500],
"stderr": result.stderr[:500],
}
except subprocess.TimeoutExpired:
return {"status": "timeout", "command": command}
# --- Discord Approval ---
def request_discord_approval(action_name: str, command: str, context: dict) -> bool:
"""Send a Discord message requesting human approval. Returns False (async approval)."""
if not DISCORD_WEBHOOK:
logger.warning("No Discord webhook configured. Blocking destructive action.")
return False
payload = {
"embeds": [{
"title": f"🔒 Approval Required: {action_name}",
"description": (
f"**Alert:** {context.get('alertname', 'unknown')}\n"
f"**Command:** `{command}`\n"
f"**Diagnosis:** {context.get('reasoning', 'N/A')}\n\n"
"React with ✅ to approve or ❌ to deny."
),
"color": 15158332,
}]
}
try:
requests.post(DISCORD_WEBHOOK, json=payload, timeout=10)
logger.info(f"Discord approval requested for {action_name}")
except Exception as e:
logger.error(f"Discord notification failed: {e}")
return False
# --- Audit Logging ---
def audit_log(entry: dict):
"""Append a structured JSON log entry."""
entry["timestamp"] = datetime.now(timezone.utc).isoformat()
with open(AUDIT_LOG, "a") as f:
f.write(json.dumps(entry) + "\n")
logger.info(f"Audit: {entry.get('event', 'unknown')} - {entry.get('action', 'none')}")
# --- Webhook Endpoint ---
@app.route("/alert", methods=["POST"])
def receive_alert():
"""Handle incoming Alertmanager webhook."""
data = request.get_json(silent=True)
if not data:
return jsonify({"error": "invalid payload"}), 400
alerts = data.get("alerts", [])
results = []
for alert in alerts:
if alert.get("status") == "resolved":
audit_log({"event": "alert_resolved", "alert": alert.get("labels", {})})
continue
alertname = alert.get("labels", {}).get("alertname", "unknown")
logger.info(f"Processing alert: {alertname}")
# Step 1: Diagnose with Ollama
diagnosis = query_ollama(alert)
if diagnosis is None:
# Fallback to remediation_hint from alert annotations
hint = alert.get("annotations", {}).get("remediation_hint")
if hint and hint in ALLOWED_ACTIONS:
logger.warning(f"Ollama unavailable. Falling back to hint: {hint}")
diagnosis = Diagnosis(
severity="unknown",
root_cause="LLM unavailable, using alert hint",
recommended_action=hint,
reasoning="Fallback to annotation-defined remediation",
)
else:
audit_log({
"event": "diagnosis_failed",
"alert": alertname,
"action": "none",
})
results.append({"alert": alertname, "status": "diagnosis_failed"})
continue
audit_log({
"event": "diagnosis",
"alert": alertname,
"severity": diagnosis.severity,
"root_cause": diagnosis.root_cause,
"action": diagnosis.recommended_action,
"reasoning": diagnosis.reasoning,
})
# Step 2: Execute remediation
context = {
"alertname": alertname,
"service": alert.get("labels", {}).get("job", ""),
"reasoning": diagnosis.reasoning,
}
result = execute_action(diagnosis.recommended_action, context)
audit_log({
"event": "remediation",
"alert": alertname,
"action": diagnosis.recommended_action,
**result,
})
results.append({
"alert": alertname,
"diagnosis": diagnosis.model_dump(),
"remediation": result,
})
return jsonify({"processed": len(results), "results": results})
@app.route("/health", methods=["GET"])
def health():
"""Health check endpoint."""
return jsonify({"status": "ok", "dry_run": DRY_RUN})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5001)
Der Empfänger bindet an 0.0.0.0:5001, damit Alertmanager ihn über Dockers Host-Gateway erreichen kann (host.docker.internal löst zur Docker-Bridge-IP auf, nicht zu 127.0.0.1). Ohne Firewall-Regel ist dieser Port dem Internet ausgesetzt. Blockieren Sie den externen Zugriff jetzt:
sudo ufw deny in on eth0 to any port 5001
Falls Sie nftables statt ufw verwenden, fügen Sie eine gleichwertige Regel hinzu, um eingehenden Verkehr auf Port 5001 von öffentlichen Interfaces zu verwerfen.
Den Empfänger mit systemd ausführen
Erstellen Sie eine systemd-Unit, damit der Empfänger beim Booten startet und bei Fehlern neu startet.
Erstellen Sie /etc/systemd/system/sentinel.service:
[Unit]
Description=Sentinel self-healing webhook receiver
After=network.target docker.service
Wants=docker.service
[Service]
Type=simple
User=sentinel
Group=sentinel
SupplementaryGroups=docker
WorkingDirectory=/home/sentinel/sentinel/receiver
ExecStart=/home/sentinel/sentinel/receiver/venv/bin/python sentinel.py
EnvironmentFile=/etc/sentinel/env
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
# Hardening
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/var/log/sentinel
[Install]
WantedBy=multi-user.target
Die Zeile SupplementaryGroups=docker gibt dem Sentinel-Prozess Zugriff auf den Docker-Socket, ohne als root zu laufen. Das ist erforderlich für die Aktionen docker restart und docker image prune.
Erstellen Sie einen dedizierten Benutzer, fügen Sie ihn der Docker-Gruppe hinzu, richten Sie sein Home-Verzeichnis ein und kopieren Sie die Empfänger-Dateien:
sudo useradd -r -m -d /home/sentinel -s /bin/false sentinel
sudo usermod -aG docker sentinel
sudo mkdir -p /etc/sentinel /var/log/sentinel
sudo chown sentinel:sentinel /var/log/sentinel
sudo cp -r ~/sentinel/receiver /home/sentinel/sentinel/receiver
sudo chown -R sentinel:sentinel /home/sentinel/sentinel
Die systemd-Unit läuft unter dem Benutzer sentinel, daher müssen der Empfänger-Code und das Virtualenv unter seinem Home-Verzeichnis liegen.
Hinweis zu Aktionen mit erhöhten Rechten: Die Aktionen
docker restartunddocker image prunefunktionieren über die Docker-Gruppenmitgliedschaft. Allerdings benötigenjournalctl --vacuum-size,kill(für Prozesse anderer Benutzer) undfallocate /swapfileRoot-Rechte. Wenn Sie diese Aktionen im Live-Modus benötigen, fügen Sie gezielte Sudoers-Regeln in/etc/sudoers.d/sentinelfür die spezifischen Befehle hinzu. Geben Sie dem Sentinel-Benutzer keinen pauschalen sudo-Zugang.
Erstellen Sie /etc/sentinel/env mit Ihren Geheimnissen:
OLLAMA_URL=http://127.0.0.1:11434
OLLAMA_MODEL=qwen2.5:7b
DISCORD_WEBHOOK=https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN
SENTINEL_DRY_RUN=true
SENTINEL_AUDIT_LOG=/var/log/sentinel/audit.jsonl
Sichern Sie die Datei ab:
sudo chmod 600 /etc/sentinel/env
sudo chown sentinel:sentinel /etc/sentinel/env
Starten Sie den Dienst:
sudo systemctl daemon-reload
sudo systemctl enable --now sentinel.service
enable sorgt dafür, dass er Neustarts übersteht. --now startet ihn sofort.
sudo systemctl status sentinel.service
● sentinel.service - Sentinel self-healing webhook receiver
Loaded: loaded (/etc/systemd/system/sentinel.service; enabled)
Active: active (running) since Wed 2026-03-19 14:35:00 UTC
Main PID: 12345 (python)
Starten Sie im Dry-Run-Modus (SENTINEL_DRY_RUN=true), bis Sie jeden Alert-Pfad getestet haben. Schalten Sie erst auf false um, wenn Sie dem Verhalten vertrauen.
Wie diagnostiziert Ollama Serverprobleme aus dem Alert-Kontext?
Ollama empfängt einen strukturierten Prompt mit dem Alert-Namen, der Schwere, der Beschreibung, den aktuellen Metrikwerten und der Liste der erlaubten Aktionen. Es gibt ein JSON-Objekt mit der Diagnose zurück: Schwerebewertung, Ursache, empfohlene Aktion und Begründung. Die Verwendung des format-Parameters von Ollama mit einem JSON-Schema erzwingt die Ausgabestruktur auf Modellebene.
Das Prompt-Template im obigen Code gibt Ollama genau das, was es braucht:
- Die Alert-Daten (was passiert ist)
- Die Liste gültiger Aktionen (was es empfehlen kann)
- Das Ausgabeschema (wie es antworten soll)
temperature: 0 macht die Ausgabe deterministisch. Derselbe Alert erzeugt immer dieselbe Diagnose.
Welche Modelle funktionieren am besten?
Nicht alle Modelle verarbeiten strukturierte JSON-Ausgabe zuverlässig. Hier ist, was wir auf einem VPS mit 4 vCPU / 8 GB RAM getestet haben:
| Modell | Größe | Inferenzzeit | JSON-Zuverlässigkeit | Anmerkungen |
|---|---|---|---|---|
| qwen2.5:7b | 4,7 GB | 3-5 s | Hoch | Bester Kompromiss zwischen Geschwindigkeit und Genauigkeit |
| llama3.1:8b | 4,7 GB | 4-6 s | Mittel | Ignoriert gelegentlich Schema-Einschränkungen |
| mistral:7b | 4,1 GB | 3-4 s | Mittel | Schnell, halluziniert aber manchmal Aktionen |
| phi3:mini | 2,3 GB | 1-2 s | Niedrig | Zu klein für zuverlässige strukturierte Ausgabe |
| qwen2.5:14b | 9,0 GB | 8-12 s | Hoch | Beste Genauigkeit, aber knapp bei 8 GB RAM |
qwen2.5:7b ist die empfohlene Standardwahl. Es passt bequem in 8 GB neben Prometheus und Ihren Diensten und erzeugt zuverlässig gültiges JSON, das dem Pydantic-Schema entspricht.
Laden Sie es herunter:
ollama pull qwen2.5:7b
Eine Beispieldiagnose für einen DiskSpaceLow-Alert:
{
"severity": "medium",
"root_cause": "Docker images and build cache accumulating over time, consuming disk space on the root filesystem",
"recommended_action": "prune_docker_images",
"reasoning": "Disk is at 87% usage. The most common cause on a Docker-based VPS is unused images. Pruning images older than 72 hours will reclaim space without affecting running containers."
}
Der Pydantic-Validator lehnt jede Antwort ab, bei der recommended_action nicht in ALLOWED_ACTIONS enthalten ist. Wenn Ollama eine Aktion wie rm -rf /tmp/* halluziniert, fängt der Validator sie ab und die Aktion wird nie ausgeführt.
Welche Gegenmaßnahmen kann das System sicher ausführen?
Die Remediation Engine führt nur Befehle aus, die in einer strikten Allowlist definiert sind. Jede Aktion hat ein festes Befehlstemplate, eine optionale Service-Allowlist und ein Flag, das angibt, ob vor der Ausführung eine menschliche Freigabe erforderlich ist.
| Aktion | Befehl | Risiko | Freigabe erforderlich |
|---|---|---|---|
| restart_service | docker restart {service} |
Niedrig | Nein (nur Container auf der Allowlist) |
| prune_docker_images | docker image prune -af --filter 'until=72h' |
Niedrig | Nein |
| kill_top_memory_process | ps aux --sort=-%mem | awk 'NR==2{print $2}' | xargs kill -15 |
Hoch | Ja |
| identify_cpu_hog | ps aux --sort=-%cpu | head -5 |
Keines (nur lesend) | Nein |
| clear_journal_logs | journalctl --vacuum-size=200M |
Niedrig | Nein |
| add_swap | fallocate + mkswap + swapon |
Mittel | Ja |
Die Aktion restart_service akzeptiert eine Template-Variable {service}, aber nur wenn der Containername in allowed_services steht. Wenn Ollama empfiehlt, sshd neu zu starten, blockiert die Engine dies, weil sshd nicht auf der Liste steht. Der Servicename kommt vom Prometheus-Label job, stellen Sie also sicher, dass Ihre job_name-Werte in prometheus.yml mit den Docker-Containernamen übereinstimmen. Falls Sie stattdessen systemd-verwaltete Dienste betreiben, tauschen Sie docker restart gegen systemctl restart.
Die Befehle verwenden SIGTERM (kill -15), nicht SIGKILL. Der Prozess bekommt die Möglichkeit, aufzuräumen. subprocess.run hat ein 60-Sekunden-Timeout, um hängende Gegenmaßnahmen zu verhindern.
Wie implementieren Sie die Sicherheitsschicht mit Allowlists und menschlicher Freigabe?
Die Sicherheitsschicht behandelt alle LLM-Ausgaben als nicht vertrauenswürdig. Drei Mechanismen verhindern gefährliche Aktionen: Die Aktions-Allowlist lehnt jeden nicht vordefinierten Befehl ab, der Pydantic-Validator fängt fehlerhafte Antworten ab, bevor sie die Engine erreichen, und das Discord-Freigabegate blockiert destruktive Aktionen, bis ein Mensch reagiert.
Verteidigung in der Tiefe
Das System hat vier Schutzschichten:
- Schema-Durchsetzung. Der
format-Parameter von Ollama beschränkt das Modell auf das JSON-Schema. Es kann keine Freitext-Befehle zurückgeben. - Pydantic-Validierung. Das
Diagnosis-Modell prüft, obrecommended_actioninALLOWED_ACTIONSexistiert. Ungültige Aktionen lösen einenValueErroraus und der Alert fällt auf den Hint-basierten Fallback zurück. - Aktions-Allowlist. Die Remediation Engine führt nur Befehle aus, die in
ALLOWED_ACTIONSdefiniert sind. Keine dynamische Befehlskonstruktion. Keine String-Interpolation über den Servicenamen hinaus (der selbst gegen eine Liste validiert wird). - Menschliche Freigabe. Aktionen mit
requires_approval: Truesenden eine Discord-Benachrichtigung und werden nicht ausgeführt. Ein separater Freigabe-Handler (außerhalb des Umfangs dieses Tutorials) lauscht auf Discord-Reaktionen und löst die Ausführung aus.
Dry-Run-Modus
Setzen Sie SENTINEL_DRY_RUN=true in /etc/sentinel/env. Der Empfänger protokolliert, was er tun würde, ohne etwas auszuführen:
{"timestamp": "2026-03-19T15:00:00Z", "event": "remediation", "alert": "DiskSpaceLow", "action": "prune_docker_images", "status": "dry_run", "command": "docker image prune -af --filter 'until=72h'"}
Lesen Sie das Audit-Log:
tail -f /var/log/sentinel/audit.jsonl | jq .
Betreiben Sie den Dry-Run-Modus mindestens eine Woche, bevor Sie die Live-Remediation aktivieren. Überprüfen Sie jede protokollierte Aktion. Stellen Sie sicher, dass das LLM sinnvolle Aktionen für Ihre tatsächlichen Alerts empfiehlt.
Discord-Freigabe-Ablauf
Wenn eine destruktive Aktion ausgelöst wird, postet der Empfänger ein Embed in Ihren Discord-Kanal:
🔒 Approval Required: kill_top_memory_process
Alert: MemoryHigh
Command: ps aux --sort=-%mem | awk 'NR==2{print $2}' | xargs kill -15
Diagnosis: Memory at 93%. The top process by RSS is consuming 4.2 GB.
React with ✅ to approve or ❌ to deny.
Ohne konfigurierten Discord-Webhook werden destruktive Aktionen vollständig blockiert. Das ist das sichere Standardverhalten. Ein fehlender Webhook bedeutet, dass keine destruktiven Aktionen ausgeführt werden, nicht dass sie ohne Freigabe laufen.
Was ist das Sentinel Pattern und warum brauchen Sie es?
Das Sentinel Pattern ist ein eigenständiger cron-basierter Watchdog, der den Monitoring-Stack selbst überwacht. Wenn Prometheus abstürzt, feuert niemand einen Alert, weil das Alertsystem down ist. Das Sentinel-Skript prüft, ob Prometheus, Alertmanager und der Webhook-Empfänger laufen, und startet sie andernfalls neu. Es ist die Antwort auf die Frage „Wer überwacht die Überwacher".
Erstellen Sie ~/sentinel/watchdog.sh:
#!/bin/bash
# Sentinel watchdog: monitors the monitoring stack
# Runs via cron every 2 minutes
LOG="/var/log/sentinel/watchdog.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
check_and_restart() {
local name="$1"
local check_cmd="$2"
local restart_cmd="$3"
if ! eval "$check_cmd" > /dev/null 2>&1; then
echo "${TIMESTAMP} ALERT: ${name} is down. Restarting..." >> "$LOG"
eval "$restart_cmd"
sleep 5
if eval "$check_cmd" > /dev/null 2>&1; then
echo "${TIMESTAMP} OK: ${name} restarted successfully" >> "$LOG"
else
echo "${TIMESTAMP} CRITICAL: ${name} failed to restart" >> "$LOG"
fi
fi
}
# Check Docker containers
check_and_restart "prometheus" \
"docker inspect --format='{{.State.Running}}' prometheus 2>/dev/null | grep -q true" \
"cd $HOME/sentinel && docker compose up -d prometheus"
check_and_restart "alertmanager" \
"docker inspect --format='{{.State.Running}}' alertmanager 2>/dev/null | grep -q true" \
"cd $HOME/sentinel && docker compose up -d alertmanager"
check_and_restart "node-exporter" \
"docker inspect --format='{{.State.Running}}' node-exporter 2>/dev/null | grep -q true" \
"cd $HOME/sentinel && docker compose up -d node-exporter"
# Check the webhook receiver systemd service
check_and_restart "sentinel-receiver" \
"systemctl is-active --quiet sentinel.service" \
"systemctl restart sentinel.service"
chmod 700 ~/sentinel/watchdog.sh
Fügen Sie einen Cron-Job hinzu, der alle 2 Minuten läuft. Ersetzen Sie /root durch Ihr tatsächliches Home-Verzeichnis:
(sudo crontab -l 2>/dev/null; echo "*/2 * * * * /root/sentinel/watchdog.sh") | sudo crontab -
Der Watchdog hat keine Abhängigkeiten außer Bash und dem Docker-CLI. Er läuft auch wenn Python, Ollama oder der gesamte Monitoring-Stack ausgefallen ist. Genau deshalb existiert er: Er ist die einzige Komponente, die von keiner anderen Komponente abhängt.
Überprüfen Sie das Watchdog-Log:
cat /var/log/sentinel/watchdog.log
2026-03-19T15:10:00Z ALERT: prometheus is down. Restarting...
2026-03-19T15:10:05Z OK: prometheus restarted successfully
Wie testen Sie die selbstheilende Schleife End-to-End?
Simulieren Sie echte Ausfälle, um die vollständige Schleife zu bestätigen: Alert feuert, Webhook empfängt, Ollama diagnostiziert, Gegenmaßnahme wird ausgeführt (oder im Dry-Run-Modus protokolliert). Starten Sie jeden Test mit SENTINEL_DRY_RUN=true, damit nichts tatsächlich ausgeführt wird, bis Sie die Diagnose überprüft haben.
Test 1: Dienst ausgefallen
Starten Sie einen Dummy-Container und stoppen Sie ihn:
docker run -d --name test-service --network sentinel_sentinel alpine sleep 3600
Fügen Sie ein Scrape-Ziel in prometheus/prometheus.yml für den Test hinzu (oder stoppen Sie einfach den node-exporter-Container):
docker stop node-exporter
Warten Sie 1-2 Minuten, bis der ServiceDown-Alert feuert. Beobachten Sie das Audit-Log:
tail -f /var/log/sentinel/audit.jsonl | jq .
{
"timestamp": "2026-03-19T15:12:30Z",
"event": "diagnosis",
"alert": "ServiceDown",
"severity": "critical",
"root_cause": "node-exporter scrape target is unreachable, container likely stopped or crashed",
"action": "restart_service",
"reasoning": "The node-exporter container is down. Restarting it will restore metric collection."
}
{
"timestamp": "2026-03-19T15:12:31Z",
"event": "remediation",
"alert": "ServiceDown",
"action": "restart_service",
"status": "dry_run",
"command": "docker restart node-exporter"
}
Starten Sie node-exporter manuell neu, um den Alert zu lösen:
docker start node-exporter
Test 2: Festplattendruck
Erstellen Sie eine große Datei, um die Festplattenauslastung über 85 % zu treiben:
fallocate -l 20G /tmp/disk-pressure-test
Warten Sie auf den DiskSpaceLow-Alert (5-Minuten-Schwellenwert). Das LLM sollte es diagnostizieren und prune_docker_images empfehlen. Nach Bestätigung des Dry-Run-Logs räumen Sie auf:
rm /tmp/disk-pressure-test
Test 3: Speicherdruck
Verwenden Sie stress-ng, um Arbeitsspeicher zu verbrauchen:
sudo apt install -y stress-ng
stress-ng --vm 1 --vm-bytes 6G --timeout 600s &
Der MemoryHigh-Alert feuert. Das LLM sollte kill_top_memory_process empfehlen, was Discord-Freigabe erfordert. Bestätigen Sie, dass die Freigabe-Nachricht in Discord erscheint.
Stoppen Sie den Stresstest:
killall stress-ng
Umschalten auf Live-Modus
Sobald jeder Dry-Run-Test korrekte Diagnosen und passende Aktionsempfehlungen liefert:
- Bearbeiten Sie
/etc/sentinel/envund setzen SieSENTINEL_DRY_RUN=false - Starten Sie den Dienst neu:
sudo systemctl restart sentinel.service - Führen Sie den Dienst-Down-Test erneut durch. Der node-exporter-Container sollte automatisch über
docker restartneu starten.
Überwachen Sie /var/log/sentinel/audit.jsonl in den ersten Tagen weiter. Jede Aktion wird mit vollständigem Kontext für Post-Incident-Reviews protokolliert.
Fehlerbehebung
Ollama antwortet nicht. Prüfen Sie, ob Ollama läuft: systemctl status ollama. Prüfen Sie, ob das Modell geladen ist: ollama list. Falls das Modell nicht geladen ist, dauert die erste Anfrage 10-20 Sekunden, während es in den RAM geladen wird.
Alertmanager kann den Webhook nicht erreichen. Überprüfen Sie, ob der Empfänger lauscht: curl http://127.0.0.1:5001/health. Überprüfen Sie die Alertmanager-Logs: docker logs alertmanager. Stellen Sie sicher, dass host.docker.internal innerhalb des Alertmanager-Containers auflöst: docker exec alertmanager wget -q -O- http://host.docker.internal:5001/health.
Alerts feuern nicht. Prüfen Sie, ob Prometheus die Regeln geladen hat: curl http://127.0.0.1:9090/api/v1/rules | python3 -m json.tool. Überprüfen Sie, ob die Scrape-Ziele aktiv sind. Eine for: 5m-Klausel bedeutet, dass die Bedingung 5 Minuten lang ununterbrochen wahr sein muss, bevor der Alert feuert.
Pydantic-Validierungsfehler. Das Modell hat eine Ausgabe erzeugt, die nicht zum Schema passt. Prüfen Sie Ollamas rohe Antwort im Journal: journalctl -u sentinel.service -f. Versuchen Sie ein anderes Modell. qwen2.5:7b ist das zuverlässigste für strukturierte Ausgabe auf begrenzter Hardware.
Watchdog läuft nicht. Überprüfen Sie den Cron-Eintrag: sudo crontab -l. Prüfen Sie den Cron-Daemon: systemctl status cron. Prüfen Sie, ob das Watchdog-Log existiert: ls -la /var/log/sentinel/watchdog.log.
Logs für jede Komponente:
# Prometheus
docker logs prometheus --tail 50
# Alertmanager
docker logs alertmanager --tail 50
# Sentinel receiver
journalctl -u sentinel.service -f
# Audit trail
tail -f /var/log/sentinel/audit.jsonl | jq .
Nächste Schritte
Das Sentinel Pattern behandelt reaktive Remediation. Für proaktives Monitoring speisen Sie Ihre Logs in ein LLM ein, um Muster zu erkennen, bevor Alerts feuern.
Wenn Sie die Remediation Engine um komplexere Skripte erweitern, sandboxen Sie diese. Lassen Sie niemals einen LLM-gesteuerten Prozess mit uneingeschränktem Systemzugang laufen.
Der Monitoring-Stack hier deckt einen einzelnen VPS ab. Für Docker-Setups mit mehreren Diensten fügen Sie cAdvisor als Scrape-Ziel hinzu, um Container-Metriken für CPU, Arbeitsspeicher und Netzwerk zu überwachen.
Bereit, es selbst auszuprobieren?
Stellen Sie Ihren eigenen Server in Sekunden bereit. Linux, Windows oder FreeBSD. →