Zentrales Log-Management mit Grafana Loki auf einem VPS
Deployen Sie Grafana Loki, Promtail und Grafana per Docker Compose auf einem einzelnen VPS. Sammeln Sie systemd-, Docker- und Nginx-Logs, fragen Sie sie mit LogQL ab und konfigurieren Sie die Aufbewahrung für den Produktionsbetrieb.
Verstreute Logs in /var/log und der Ausgabe von docker logs werden unübersichtlich, sobald Sie mehr als zwei Dienste betreiben. Dieses Tutorial deployt den Grafana + Loki + Promtail Stack auf einem einzelnen VPS mit Docker Compose. Sie sammeln Logs von systemd, Docker-Containern und Nginx, fragen sie mit LogQL ab, konfigurieren die Aufbewahrung, damit Logs nicht Ihre Festplatte füllen, und sichern den Stack für einen öffentlich erreichbaren Server ab.
Am Ende ist Lokis HTTP-API bereit für programmatische Abfragen. Der Artikel KI-Log-Analyse mit Ollama auf einem VPS: Anomalien mit einem lokalen LLM erkennen baut darauf auf, um Logs an ein lokales LLM zur Anomalieerkennung zu übergeben.
Was macht der Grafana + Loki + Promtail Stack?
Grafana Loki ist ein Open-Source-System zur Log-Aggregation, das nur Log-Labels (Metadaten) indexiert, nicht den vollständigen Text der Log-Einträge. Das macht es deutlich ressourcenschonender als Elasticsearch. Loki speichert komprimierte Log-Blöcke auf dem Dateisystem oder in Objektspeicher. Zusammen mit Promtail für die Sammlung und Grafana für die Visualisierung bildet es einen vollständigen zentralen Logging-Stack.
Die drei Komponenten haben unterschiedliche Aufgaben:
| Komponente | Aufgabe | Ressourcenbedarf |
|---|---|---|
| Loki | Empfängt, speichert und indexiert Logs. Beantwortet Abfragen. | 300-600 MB RAM im Leerlauf, bis zu 1 GB bei intensiven Abfragen |
| Promtail | Entdeckt Log-Quellen, liest Dateien fortlaufend, sendet Einträge an Loki | 50-100 MB RAM |
| Grafana | Web-Oberfläche zum Abfragen und Visualisieren von Logs über Explore | 200-300 MB RAM |
Gesamter Stack-Bedarf: 1-1,5 GB RAM. Ein 2 GB VPS ist das Minimum. Ein 4 GB VPS bietet komfortablen Spielraum für LogQL-Abfragen auf größeren Datensätzen.
Loki vs Elasticsearch: Elasticsearch indexiert jedes Wort in jeder Log-Zeile, was Volltextsuche ermöglicht, aber 10-20x mehr RAM und Festplatte kostet. Lokis Label-basierter Index bedeutet: Sie filtern zuerst nach Labels, dann durchsuchen Sie die passenden Blöcke. Für die meisten VPS-Workloads ist das der richtige Kompromiss. Wenn Sie Volltextsuche über Terabytes von Logs benötigen, ist Loki nicht das richtige Werkzeug.
Voraussetzungen
- Ein VPS mit mindestens 2 GB RAM (4 GB empfohlen). Ein Virtua Cloud VPS mit 4 vCPU und 8 GB RAM bewältigt diesen Stack problemlos.
- Docker und Docker Compose installiert. Falls Sie Hilfe bei der Einrichtung benötigen, siehe [-> docker-compose-multi-service-vps].
- Ein Nicht-Root-Benutzer mit sudo-Zugriff.
- Grundkenntnisse im Terminal und in YAML-Syntax.
Prüfen Sie, ob Docker läuft:
docker --version
docker compose version
Sie sollten Docker 24+ und Compose v2+ sehen. Wenn einer der Befehle fehlschlägt, ist Docker nicht installiert oder das Compose-Plugin fehlt.
Wie deploye ich Loki mit Docker Compose auf einem VPS?
Erstellen Sie ein Projektverzeichnis und drei Konfigurationsdateien: docker-compose.yml, loki-config.yml und promtail-config.yml. Die Docker-Compose-Datei pinnt alle Images auf bestimmte Versionen, setzt Ressourcenlimits, konfiguriert persistente Volumes und bindet Loki nur an localhost.
Projektstruktur
mkdir -p ~/loki-stack/{loki-data,promtail-data}
cd ~/loki-stack
Das Verzeichnis loki-data enthält Chunks, Indizes und das Write-Ahead-Log. Das Verzeichnis promtail-data speichert Promtails Positionsdatei, damit es nach Neustarts fortfahren kann.
Setzen Sie den Besitzer des Loki-Datenverzeichnisses. Das Loki 3.x Docker-Image läuft als UID 10001, nicht als root. Ohne diese Änderung schlägt der Loki-Start mit „permission denied" beim Anlegen von Unterverzeichnissen fehl:
chown 10001:10001 ~/loki-stack/loki-data
docker-compose.yml
services:
loki:
image: grafana/loki:3.6.7
command: -config.file=/etc/loki/config.yml
volumes:
- ./loki-config.yml:/etc/loki/config.yml:ro
- ./loki-data:/loki
ports:
- "127.0.0.1:3100:3100"
restart: unless-stopped
deploy:
resources:
limits:
memory: 1g
networks:
- loki-net
promtail:
image: grafana/promtail:3.6.7
command: -config.file=/etc/promtail/config.yml
volumes:
- ./promtail-config.yml:/etc/promtail/config.yml:ro
- ./promtail-data:/var/lib/promtail
- /var/log:/var/log:ro
- /var/log/journal:/var/log/journal:ro
- /run/log/journal:/run/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
deploy:
resources:
limits:
memory: 256m
depends_on:
- loki
networks:
- loki-net
grafana:
image: grafana/grafana:11.5.2
volumes:
- grafana-data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD__FILE=/run/secrets/grafana_admin_pw
- GF_SERVER_ROOT_URL=http://localhost:3000
- GF_USERS_ALLOW_SIGN_UP=false
- GF_ANALYTICS_REPORTING_ENABLED=false
secrets:
- grafana_admin_pw
restart: unless-stopped
deploy:
resources:
limits:
memory: 512m
depends_on:
- loki
networks:
- loki-net
secrets:
grafana_admin_pw:
file: ./secrets/grafana_admin_pw
volumes:
grafana-data:
networks:
loki-net:
driver: bridge
Achten Sie darauf: Loki und Grafana binden an 127.0.0.1, nicht an 0.0.0.0. Das verhindert externen Zugriff. Sie greifen auf Grafana über einen SSH-Tunnel oder einen Reverse Proxy zu. Loki oder Grafana direkt im Internet freizugeben ist ein häufiger Fehler, der in den meisten Online-Tutorials vorkommt.
Die Richtlinie restart: unless-stopped stellt sicher, dass jeder Dienst Neustarts überlebt. Wenn Sie einen Dienst manuell mit docker compose stop anhalten, bleibt er gestoppt. Andernfalls startet er automatisch neu.
Gepinnte Image-Tags (3.6.7, 11.5.2) verhindern unerwartete Upgrades. Verwenden Sie niemals :latest in der Produktion. Wenn Sie upgraden möchten, ändern Sie den Tag und führen Sie docker compose up -d aus, um das neue Image zu laden.
Grafana-Admin-Passwort generieren
Verwenden Sie niemals Standard-Anmeldedaten. Generieren Sie ein starkes Passwort und speichern Sie es in einer Secrets-Datei mit eingeschränkten Berechtigungen:
mkdir -p ~/loki-stack/secrets
openssl rand -base64 32 > ~/loki-stack/secrets/grafana_admin_pw
chmod 644 ~/loki-stack/secrets/grafana_admin_pw
Prüfen Sie die Berechtigungen:
ls -la ~/loki-stack/secrets/grafana_admin_pw
Sie sollten -rw-r--r-- sehen. Die Datei muss für alle lesbar sein, da Docker Compose (außerhalb des Swarm-Modus) dateibasierte Secrets mit den Berechtigungen der Quelldatei einbindet. Grafana läuft als UID 472 im Container und benötigt Lesezugriff. Die Datei ist trotzdem durch ihren Speicherort in einem dedizierten Secrets-Verzeichnis geschützt, und nur der root-Benutzer des Hosts kann sie ändern. Die Umgebungsvariable GF_SECURITY_ADMIN_PASSWORD__FILE weist Grafana an, das Passwort beim Start aus dieser Datei zu lesen, anstatt es in die Compose-Datei einzubetten.
loki-config.yml
Diese Konfiguration verwendet TSDB mit Schema v13 (Loki 3.x Standard), Dateisystemspeicher für Single-Node-Deployment und den Compactor für die Aufbewahrung:
auth_enabled: false
server:
http_listen_port: 3100
http_listen_address: 0.0.0.0
log_level: warn
http_server_read_timeout: 30s
http_server_write_timeout: 30s
common:
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
replication_factor: 1
path_prefix: /loki
schema_config:
configs:
- from: "2024-01-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
storage_config:
filesystem:
directory: /loki/chunks
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem
limits_config:
retention_period: 720h
max_streams_per_user: 10000
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
max_label_names_per_series: 15
ingester:
chunk_encoding: snappy
wal:
dir: /loki/wal
enabled: true
chunk_store_config:
chunk_cache_config:
embedded_cache:
enabled: true
max_size_mb: 100
Wichtige Entscheidungen in dieser Konfiguration:
auth_enabled: falseist hier sicher, da Loki nur auf localhost lauscht (Docker-internes Netzwerk + 127.0.0.1 Port-Binding). Multi-Tenant-Setups benötigenauth_enabled: truemit einemX-Scope-OrgID-Header bei jeder Anfrage.retention_period: 720hbehält Logs für 30 Tage. Loki 3.x verwendet standardmäßig0s(unbegrenzt aufbewahren), wenn Sie diesen Wert nicht setzen. Ihre Festplatte wird volllaufen.schema: v13mitstore: tsdbist für Loki 3.x Funktionen erforderlich. Ältereboltdb-shipper-Konfigurationen aus Loki 2.x Tutorials werden beim Start fehlschlagen oder Deprecation-Warnungen ausgeben.chunk_encoding: snappykomprimiert Chunks mit Snappy. Schneller als gzip, etwas größere Dateien. Guter Standard für Single-Node, wo CPU stärker eingeschränkt ist als Festplattenplatz.- WAL aktiviert: Das Write-Ahead-Log schützt vor Datenverlust, falls Loki während des Schreibens abstürzt. Nach dem Neustart spielt Loki das WAL ab, um nicht gespeicherte Einträge wiederherzustellen. Sie werden beim Start „WAL replay"-Meldungen in den Logs sehen. Das ist normal.
max_label_names_per_series: 15entspricht dem Loki 3.x Standard. Halten Sie die Label-Kardinalität niedrig. Labels wieuser_idoderrequest_iderzeugen zu viele Streams und verschlechtern die Performance.
Wie konfiguriere ich Promtail für systemd-Journal-Logs?
Promtail sammelt Logs aus mehreren Quellen und sendet sie an Loki. Die folgende Konfiguration sammelt aus drei Quellen in separaten scrape_configs-Jobs: systemd-Journal, Docker-Container und Nginx-Log-Dateien.
Promtail End-of-Life Hinweis: Promtail hat am 2. März 2026 sein End-of-Life erreicht. Grafana Alloy ist der offizielle Nachfolger. Dieses Tutorial verwendet Promtail, weil die Konfigurationskonzepte direkt auf Alloy übertragbar sind und Millionen von Deployments es noch verwenden. Siehe den Abschnitt „Ist Promtail veraltet?" weiter unten für Migrationsschritte.
promtail-config.yml
server:
http_listen_port: 9080
log_level: warn
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# --- Systemd journal ---
- job_name: journal
journal:
max_age: 12h
labels:
job: systemd-journal
relabel_configs:
- source_labels: ['__journal__systemd_unit']
target_label: unit
- source_labels: ['__journal_priority_keyword']
target_label: severity
- source_labels: ['__journal__hostname']
target_label: hostname
# --- Docker containers ---
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.+)'
target_label: container
- source_labels: ['__meta_docker_container_log_stream']
target_label: stream
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: compose_service
# --- Nginx access and error logs ---
- job_name: nginx
static_configs:
- targets:
- localhost
labels:
job: nginx
type: access
__path__: /var/log/nginx/access.log
- targets:
- localhost
labels:
job: nginx
type: error
__path__: /var/log/nginx/error.log
Die Datei positions.yaml verfolgt, wie weit Promtail in jeder Log-Quelle gelesen hat. Wenn Promtail neu startet, setzt es dort fort, wo es aufgehört hat, anstatt alte Logs erneut zu senden oder neue zu verpassen.
Wie der Journal-Job funktioniert
Der journal-Block liest direkt aus dem systemd-Journal über die Journal-API. Die Einstellung max_age: 12h weist Promtail an, beim ersten Start nur Journal-Einträge der letzten 12 Stunden aufzunehmen. Ohne diese Einstellung würde Promtail versuchen, den gesamten Journal-Verlauf aufzunehmen, der auf lang laufenden Servern Gigabytes umfassen kann.
Die relabel_configs extrahieren Metadaten aus Journal-Einträgen in Loki-Labels. __journal__systemd_unit wird zum Label unit (z.B. sshd.service, nginx.service). __journal_priority_keyword wird zu severity (z.B. warning, err, info). Diese Labels ermöglichen effizientes Filtern in LogQL, ohne jede Zeile durchsuchen zu müssen.
Damit das Journal-Scraping funktioniert, benötigt der Promtail-Container zwei Volume-Mounts: /run/log/journal (oder /var/log/journal, wenn Ihr System Logs persistent speichert) und /etc/machine-id. Die Machine-ID identifiziert, welches Journal gelesen werden soll.
Docker-Image-Einschränkung: Das Standard-Docker-Image
grafana/promtailist nicht mit systemd-Journal-Unterstützung kompiliert. Wenn Siesupport for reading the systemd journal is not compiled into this build of promtailin den Logs sehen, funktioniert das Journal-Scraping nicht aus dem Docker-Container. Sie haben zwei Optionen:
- Promtail als Host-Binary installieren statt das Docker-Image zu verwenden. Laden Sie es von der Loki-Releases-Seite herunter und führen Sie es direkt auf dem Host aus, wo es nativen Zugriff auf die Journal-API hat.
- Grafana Alloy verwenden (siehe den Migrationsabschnitt unten), das Journal-Scraping in seinem Docker-Image unterstützt.
Die Docker- und Nginx-Datei-Scraping-Jobs funktionieren im Docker-Image einwandfrei. Nur das Journal-Scraping erfordert das Host-Binary oder Alloy.
Wie sammle ich Docker-Container-Logs mit Promtail?
Der docker-Job nutzt Docker Service Discovery, um automatisch alle laufenden Container auf dem Host zu entdecken. Promtail verbindet sich über /var/run/docker.sock mit dem Docker-Daemon und fragt alle 5 Sekunden nach neuen Containern. Wenn ein Container startet oder stoppt, beginnt oder beendet Promtail automatisch das Lesen seiner Logs.
Die relabel_configs extrahieren nützliche Metadaten:
__meta_docker_container_namewird zum Labelcontainer. Die Regex'/(.+)'entfernt den führenden/, den Docker an Containernamen anfügt.__meta_docker_container_log_streamwird zum Labelstream(stdoutoderstderr).__meta_docker_container_label_com_docker_compose_serviceextrahiert den Compose-Dienstnamen (z.B.loki,grafana). Dieses Label existiert nur für Container, die von Docker Compose verwaltet werden.
Sie müssen keine einzelnen Container konfigurieren. Jeder Container mit einem Log-Treiber, der auf das Dateisystem schreibt (der Standard-json-file-Treiber), wird erkannt. Wenn Sie einen Datenbankcontainer, eine Webanwendung und einen Cache betreiben, erscheinen alle drei automatisch in Loki unter ihren Containernamen.
Um bestimmte Container von der Log-Sammlung auszuschließen, fügen Sie ein Docker-Label hinzu und filtern darauf:
relabel_configs:
# ... existing relabel rules ...
- source_labels: ['__meta_docker_container_label_logging']
regex: 'disabled'
action: drop
Dann auf dem Container, den Sie ausschließen möchten:
noisy-service:
image: some/image
labels:
logging: "disabled"
Wie scrape ich Nginx-Access- und Error-Logs mit Promtail?
Der nginx-Job verwendet static_configs mit dem Label __path__, um bestimmte Log-Dateien zu überwachen. Im Gegensatz zur Docker Service Discovery müssen Sie die Log-Dateipfade im Voraus kennen. Nginx schreibt standardmäßig nach /var/log/nginx/access.log und /var/log/nginx/error.log.
Das Label type unterscheidet zwischen Access- und Error-Logs. Das ermöglicht separate Abfragen in LogQL:
{job="nginx", type="access"} # nur Access-Logs
{job="nginx", type="error"} # nur Error-Logs
{job="nginx"} # beide
Wenn Nginx nicht auf dem Host installiert ist, protokolliert Promtail eine Warnung über fehlende Dateien, sammelt aber weiter aus anderen Quellen. Das ist harmlos. Entfernen Sie den Nginx-Job aus der Konfiguration, wenn Sie Nginx nicht verwenden.
Für Nginx in einem Docker-Container haben Sie zwei Optionen. Sie können die Docker Service Discovery verwenden (stdout/stderr des Containers werden automatisch erfasst). Oder Sie mounten das Nginx-Log-Verzeichnis als gemeinsames Volume und verwenden den statischen Datei-Scraper. Der Docker-Ansatz ist einfacher. Der Datei-Ansatz gibt Ihnen separate Labels type: access und type: error.
Stack starten
cd ~/loki-stack
docker compose up -d
Prüfen Sie, ob alle drei Container laufen:
docker compose ps
Sie sollten loki, promtail und grafana mit dem Status Up sehen. Falls ein Dienst Restarting zeigt, prüfen Sie seine Logs:
docker compose logs <service-name> --tail=30
Prüfen Sie, ob Loki bereit ist:
curl -s http://127.0.0.1:3100/ready
Erwartete Ausgabe: ready. Wenn Sie Ingester not ready: waiting for 15s after being ready erhalten, warten Sie 15 Sekunden und versuchen Sie es erneut. Loki benötigt Zeit, um den Ingester-Ring zu initialisieren.
Prüfen Sie die Promtail-Targets:
docker compose logs promtail --tail=20
Suchen Sie nach Zeilen, die entdeckte Targets zeigen. Sie sollten Einträge für das Journal, den Docker-Socket und die Nginx-Log-Pfade sehen. Es sollten keine level=error-Zeilen erscheinen.
Wie überprüfe ich, ob Logs in Grafana Explore erscheinen?
Öffnen Sie einen SSH-Tunnel, um von Ihrem lokalen Rechner auf Grafana zuzugreifen. Wir verwenden einen Tunnel, weil Grafana auf dem VPS an localhost gebunden ist und nicht aus dem Internet erreichbar ist.
ssh -L 3000:127.0.0.1:3000 user@your-vps-ip
Öffnen Sie http://localhost:3000 in Ihrem Browser. Melden Sie sich mit dem Benutzernamen admin und dem Passwort aus ~/loki-stack/secrets/grafana_admin_pw an. Lesen Sie es mit:
cat ~/loki-stack/secrets/grafana_admin_pw
Loki als Datenquelle hinzufügen
- Gehen Sie zu Connections > Data Sources > Add data source
- Wählen Sie Loki
- Setzen Sie die URL auf
http://loki:3100(das ist der Docker-interne Hostname, nicht localhost) - Klicken Sie auf Save & test
Sie sollten „Data source successfully connected." sehen. Falls es fehlschlägt, überprüfen Sie, ob beide Container im selben Docker-Netzwerk (loki-net) sind.
Erste Abfrage ausführen
- Gehen Sie zu Explore (Kompass-Symbol in der Seitenleiste)
- Wählen Sie die Loki-Datenquelle
- Wechseln Sie in den Code-Modus (nicht Builder) und geben Sie diese LogQL-Abfrage ein:
{job="systemd-journal"} |= "ssh"
Das zeigt alle systemd-Journal-Einträge mit „ssh". Wenn Sie Log-Zeilen sehen, funktioniert die gesamte Pipeline: Journal -> Promtail -> Loki -> Grafana.
Probieren Sie eine Docker-Container-Abfrage:
{compose_service="loki"}
Das gibt Lokis eigene Logs zurück, von Promtail über Docker Service Discovery gesammelt.
Und eine Nginx-Abfrage (falls Nginx installiert ist und Logs erzeugt):
{job="nginx", type="error"}
Falls Grafana „No data" anzeigt, warten Sie 2-3 Minuten. Loki benötigt Zeit, um den ersten Batch von Logs aufzunehmen und zu indexieren.
Welche LogQL-Abfragen sind für Server-Logs am nützlichsten?
LogQL hat zwei Abfragetypen: Log-Abfragen geben Log-Zeilen zurück, Metrik-Abfragen geben numerische Werte zurück. Beide beginnen mit einem Stream-Selektor ({label="value"}), der die zu durchsuchenden Log-Streams auswählt, und fügen dann Filter und Parser hinzu, um die Ergebnisse zu verfeinern.
Stream-Selektoren und Zeilenfilter
# Alle Logs vom SSH-Daemon
{unit="sshd.service"}
# Zeilen mit "Failed password"
{unit="sshd.service"} |= "Failed password"
# Zeilen OHNE "Accepted"
{unit="sshd.service"} != "Accepted"
# Regex-Match: IP-Adressen
{unit="sshd.service"} |~ "\\d+\\.\\d+\\.\\d+\\.\\d+"
# Groß-/Kleinschreibung ignorieren
{job="nginx", type="error"} |~ "(?i)timeout"
Zeilenfilter (|=, !=, |~, !~) laufen nach dem Stream-Selektor. Sie durchsuchen den Inhalt der Log-Zeilen. Mehrere Filter werden verkettet und müssen alle zutreffen:
{unit="sshd.service"} |= "Failed" |= "root"
Das findet Zeilen, die sowohl „Failed" als auch „root" enthalten.
Parser
Parser extrahieren strukturierte Felder aus unstrukturierten Log-Zeilen. Nach dem Parsen können Sie auf extrahierte Felder wie status >= 500 filtern, statt Regex zu verwenden. Wählen Sie den richtigen Parser für Ihr Log-Format:
| Parser | Syntax | Geeignet für | Performance-Hinweise |
|---|---|---|---|
logfmt |
| logfmt |
Key=Value-Logs (systemd, Go-Apps) | Am schnellsten. Kein Regex. |
json |
| json |
JSON-strukturierte Logs | Schnell. Natives JSON-Parsing. |
pattern |
| pattern "<pattern>" |
Logs mit festem Format (Nginx Combined, Apache) | Schnell. Positionsbasierte Extraktion. |
regexp |
| regexp "<regex>" |
Unregelmäßige Formate, gemischte Strukturen | Am langsamsten. Nur als letztes Mittel. |
Verwenden Sie logfmt oder json, wenn Ihre Logs bereits strukturiert sind. Verwenden Sie pattern für bekannte Formate wie das Nginx Combined Log. Verwenden Sie regexp nur, wenn nichts anderes funktioniert, da Regex-Parsing auf Streams mit hohem Volumen deutlich langsamer ist.
Nginx-Access-Log mit Pattern-Parser:
{job="nginx", type="access"}
| pattern "<ip> - - [<timestamp>] \"<method> <uri> <_>\" <status> <bytes>"
| status >= 500
Das parst das Nginx Combined Log Format in benannte Felder und filtert nach 5xx-Fehlern. Der Platzhalter <_> verwirft nicht benötigte Felder (in diesem Fall die HTTP-Version).
JSON-Log-Parser:
{compose_service="myapp"}
| json
| level="error"
| line_format "{{.timestamp}} {{.message}}"
Die line_format-Stufe formatiert die Ausgabe um. Nützlich, wenn JSON-Logs unübersichtlich sind und Sie eine sauberere Ausgabe in Grafana wünschen.
Metriken aus Logs
Metrik-Abfragen wandeln Log-Zeilen in Zahlen um. Sie treiben Grafana-Dashboards und Alerting-Regeln an:
# Rate fehlgeschlagener SSH-Logins pro Minute über die letzte Stunde
rate({unit="sshd.service"} |= "Failed password" [5m])
# Gesamtzahl Nginx 5xx-Fehler in 5-Minuten-Fenstern
count_over_time(
{job="nginx", type="access"}
| pattern "<_> - - [<_>] \"<_> <_> <_>\" <status> <_>"
| status >= 500
[5m]
)
# P95 Antwortzeit aus JSON-App-Logs
# unwrap extrahiert ein numerisches Feld für die Aggregation
quantile_over_time(0.95,
{compose_service="myapp"}
| json
| unwrap response_time_ms
| __error__=""
[5m]
) by (endpoint)
# Von Nginx ausgelieferte Bytes pro Sekunde
sum(rate(
{job="nginx", type="access"}
| pattern "<_> - - [<_>] \"<_> <_> <_>\" <_> <bytes>"
| unwrap bytes
| __error__=""
[5m]
))
Der Filter | __error__="" nach unwrap verwirft Zeilen, bei denen die numerische Extraktion fehlgeschlagen ist (nicht-numerische Werte, fehlende Felder). Ohne ihn erzeugen diese Zeilen stillschweigend Nullwerte und verfälschen Ihre Ergebnisse. Fügen Sie diesen Filter immer nach unwrap hinzu.
Der Range [5m] definiert die Fenstergröße. Kürzere Ranges (1m) liefern granularere, aber verrauschtere Daten. Längere Ranges (15m, 1h) glätten Spitzen. Für Dashboards ist 5m ein guter Ausgangspunkt.
Wie konfiguriere ich die Log-Aufbewahrung in Loki 3.x?
In Loki 3.x wird die Aufbewahrung vom Compactor verwaltet. Setzen Sie retention_period unter limits_config und aktivieren Sie den Compactor mit retention_enabled: true. Die Standard-Aufbewahrung in Loki 3.0+ ist 0s (unbegrenzt aufbewahren), daher müssen Sie dies explizit konfigurieren, sonst läuft Ihre Festplatte voll.
Die obige loki-config.yml enthält bereits die Aufbewahrung. So interagieren die Einstellungen:
compactor:
retention_enabled: true # Muss true sein, sonst kompaktiert der Compactor nur (keine Löschung)
retention_delete_delay: 2h # 2h nach Markierung der Chunks warten vor dem Löschen
compaction_interval: 10m # Wie oft der Compactor läuft
limits_config:
retention_period: 720h # 30 Tage globaler Standard
Der Compactor läuft als Teil des Loki-Prozesses im Single-Node-Modus. Er durchsucht den TSDB-Index, identifiziert Chunks, die älter als die Aufbewahrungsfrist sind, markiert sie zur Löschung und entfernt sie nach dem retention_delete_delay. Die Verzögerung gibt Ihnen ein Zeitfenster zur Wiederherstellung bei Fehlkonfiguration.
Aufbewahrung pro Stream
Sie können verschiedene Aufbewahrungsfristen für verschiedene Log-Streams festlegen. Logs mit hohem Volumen und geringem Wert (wie Debug-Ausgaben) können schneller ablaufen:
limits_config:
retention_period: 720h
retention_stream:
- selector: '{job="nginx", type="access"}'
priority: 1
period: 336h # 14 Tage für Access-Logs
- selector: '{severity="debug"}'
priority: 2
period: 72h # 3 Tage für Debug-Logs
Höhere Prioritätswerte gewinnen, wenn mehrere Selektoren denselben Stream treffen. Ein Debug-Level Nginx Access Log trifft auf beide Regeln zu. Priorität 2 gewinnt, daher erhält es 3 Tage Aufbewahrung.
Dimensionierung der Aufbewahrung
Schätzen Sie den Festplattenverbrauch, bevor Sie sich auf eine Aufbewahrungsfrist festlegen. Loki komprimiert Logs gut (5-10x Verhältnis mit Snappy), aber die Zahlen summieren sich auf aktiven Servern:
| Rohes Log-Volumen/Tag | Komprimiert (geschätzt) | 7-Tage-Aufbewahrung | 30-Tage-Aufbewahrung | 90-Tage-Aufbewahrung |
|---|---|---|---|---|
| 100 MB | ~15 MB | ~105 MB | ~450 MB | ~1,35 GB |
| 500 MB | ~75 MB | ~525 MB | ~2,25 GB | ~6,75 GB |
| 1 GB | ~150 MB | ~1,05 GB | ~4,5 GB | ~13,5 GB |
| 5 GB | ~750 MB | ~5,25 GB | ~22,5 GB | ~67,5 GB |
Dies sind Schätzungen. Die tatsächliche Komprimierung hängt vom Log-Inhalt ab. Repetitive Logs (Access-Logs mit ähnlichen Pfaden) komprimieren besser als zufällige Debug-Ausgaben. Überwachen Sie den tatsächlichen Verbrauch mit:
du -sh ~/loki-stack/loki-data/
Führen Sie diesen Befehl wöchentlich aus, um unerwartetes Wachstum zu erkennen, bevor der Festplattenplatz ausgeht.
Wie optimiere ich Loki für die Produktion auf einem einzelnen VPS?
Eine Standard-Loki-Konfiguration funktioniert für Tests, benötigt aber Tuning für einen Produktions-VPS. Die folgenden Änderungen reduzieren Speicherspitzen, schützen vor unkontrollierten Log-Streams und härten den Stack für einen öffentlich erreichbaren Server.
Loki an localhost binden
Bereits in der obigen docker-compose.yml erledigt (127.0.0.1:3100:3100). Prüfen Sie nach dem Deployment:
ss -tlnp | grep 3100
Sie sollten 127.0.0.1:3100 sehen, nicht 0.0.0.0:3100. Machen Sie dasselbe für Grafana auf Port 3000.
Firewall-Regeln
Wenn Sie ufw verwenden, blockieren Sie externen Zugriff auf die Logging-Ports:
sudo ufw deny 3100/tcp comment "Loki - localhost only"
sudo ufw deny 3000/tcp comment "Grafana - localhost only"
sudo ufw status numbered
Da die Ports bereits an localhost gebunden sind, ist die Firewall eine zusätzliche Schutzschicht (Defense in Depth). Falls jemand versehentlich die Compose-Datei ändert, um auf 0.0.0.0 zu binden, blockiert die Firewall weiterhin externen Zugriff.
Versionsinformationen verbergen
Die Offenlegung von Versionen hilft Angreifern, bekannte Schwachstellen gezielt auszunutzen. GF_ANALYTICS_REPORTING_ENABLED=false in der Compose-Datei deaktiviert bereits die Grafana-Telemetrie. Lokis Endpunkt /loki/api/v1/status/buildinfo gibt Versionsdetails preis, aber da Loki an localhost gebunden ist, können nur lokale Prozesse darauf zugreifen.
Wenn Sie Grafana hinter einem Reverse Proxy (Nginx, Caddy) betreiben, fügen Sie diese Nginx-Einstellungen hinzu:
server_tokens off;
proxy_hide_header X-Powered-By;
Wichtige Breaking Changes in Loki 3.x
Wenn Sie von einer Loki 2.x Konfiguration migrieren oder einem älteren Tutorial folgen, beachten Sie diese Änderungen:
| Änderung | Loki 2.x Standard | Loki 3.x Standard | Erforderliche Aktion |
|---|---|---|---|
| Schema | v11/v12 | v13 | Verwenden Sie schema: v13 mit store: tsdb |
| Index Store | boltdb-shipper |
tsdb |
Migrieren Sie zu TSDB (BoltDB veraltet) |
| Aufbewahrung | 0s (unbegrenzt) | 0s (unbegrenzt) | Setzen Sie retention_period explizit |
| Strukturierte Metadaten | Deaktiviert | Aktiviert | Erfordert Schema v13 |
| Max Labels pro Serie | 30 | 15 | Reduzieren Sie die Label-Kardinalität oder erhöhen Sie das Limit |
| Docker-Image | Enthält BusyBox-Shell | Keine Shell | docker exec in den Container nicht möglich |
Das Shell-lose Docker-Image in Loki 3.6+ bedeutet, dass Sie docker exec -it loki sh nicht zum Debuggen verwenden können. Prüfen Sie stattdessen Logs mit docker compose logs loki und die Verfügbarkeit mit curl http://127.0.0.1:3100/ready.
Dateisystem vs Objektspeicher
Für einen einzelnen VPS ist Dateisystemspeicher die richtige Wahl. Objektspeicher (S3, GCS, MinIO) fügt Komplexität und Latenz hinzu, die sich nur lohnt, wenn Sie Folgendes benötigen:
- Mehrere Loki-Instanzen, die dieselben Daten teilen
- Unbegrenzten Speicher über die VPS-Festplatte hinaus
- Regionsübergreifende Replikation
Bleiben Sie beim Dateisystemspeicher, bis Sie über einen einzelnen Node hinauswachsen.
Stack überwachen
Prüfen Sie die Ressourcennutzung der laufenden Container:
docker stats --no-stream
Prüfen Sie Loki-Logs auf Warnungen und Fehler:
docker compose logs loki --tail=50 | grep -E "level=(error|warn)"
Häufige Probleme und Lösungen:
stream limit exceeded: Erhöhen Siemax_streams_per_userinlimits_config. Meist durch Labels mit hoher Kardinalität verursacht.ingestion rate limit reached: Erhöhen Sieingestion_rate_mb. Tritt bei Log-Bursts auf (Deployments, Fehlerstürme).WAL replay: Normal beim Start. Loki stellt nicht gespeicherte Schreibvorgänge aus dem Write-Ahead-Log wieder her.- Hohe Speichernutzung: Reduzieren Sie
max_size_mbinchunk_cache_configoder verringern Sieingestion_burst_size_mb.
Ist Promtail veraltet? Sollte ich stattdessen Grafana Alloy verwenden?
Ja. Promtail ging am 13. Februar 2025 in den Long-Term-Support und erreichte am 2. März 2026 sein End-of-Life. Es werden keine weiteren Updates, Bugfixes oder Sicherheitspatches veröffentlicht. Grafana Alloy ist der offizielle Ersatz. Es ist Grafana Labs' Distribution des OpenTelemetry Collectors und verarbeitet Logs, Metriken, Traces und Profiling-Daten in einem einzigen Agenten.
Warum dieses Tutorial noch Promtail verwendet
Die Konfigurationskonzepte von Promtail lassen sich direkt auf Alloy übertragen. Die Scrape-Configs, Relabel-Regeln und Pipeline-Stufen funktionieren gleich. Promtail zu lernen ist weiterhin nützlich, weil:
- Millionen bestehender Deployments es verwenden
- Das Alloy-Migrationstool Promtail-Configs automatisch konvertiert
- Promtail-Verständnis das Debugging von Alloy-Konfigurationen erleichtert
- Das Suchvolumen für Promtail-Tutorials hoch bleibt und die Konzepte übertragbar sind
Migration zu Alloy
Konvertieren Sie Ihre Promtail-Konfiguration ins Alloy-Format mit einem Befehl:
alloy convert --source-format=promtail --output=alloy-config.alloy promtail-config.yml
Das erzeugt eine Konfigurationsdatei im Alloy-Format. Prüfen Sie die Ausgabe vor dem Deployment. Der Konverter behandelt die meisten Fälle, kann aber bei benutzerdefinierten Pipeline-Stufen manuelle Anpassungen erfordern.
Ersetzen Sie dann den Promtail-Dienst in docker-compose.yml:
alloy:
image: grafana/alloy:v1.14.1
command:
- run
- /etc/alloy/config.alloy
- --server.http.listen-addr=0.0.0.0:12345
volumes:
- ./alloy-config.alloy:/etc/alloy/config.alloy:ro
- /var/log:/var/log:ro
- /var/log/journal:/var/log/journal:ro
- /run/log/journal:/run/log/journal:ro
- /etc/machine-id:/etc/machine-id:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
depends_on:
- loki
networks:
- loki-net
Entfernen Sie den promtail-Dienstblock und führen Sie docker compose up -d aus. Alloy beginnt mit der Sammlung derselben Log-Quellen unter Verwendung der konvertierten Konfiguration.
Für neue Deployments von Grund auf: Verwenden Sie Alloy von Anfang an. Für bestehende Promtail-Setups: Planen Sie die Migration, aber es besteht keine unmittelbare Dringlichkeit. Das Promtail-Binary funktioniert weiterhin. Pinnen Sie den Image-Tag (grafana/promtail:3.6.7), damit Sie kontrollieren, was läuft.
Wie frage ich Loki-Logs programmatisch über die HTTP-API ab?
Loki stellt eine REST-API für programmatische Log-Abfragen bereit. So integrieren Sie Loki mit Skripten, Alerting-Pipelines oder der KI-Log-Analyse-Schicht aus KI-Log-Analyse mit Ollama auf einem VPS: Anomalien mit einem lokalen LLM erkennen.
Die API akzeptiert dieselben LogQL-Abfragen, die Sie in Grafana verwenden. Der Haupt-Endpunkt ist /loki/api/v1/query_range für Zeitbereichsabfragen.
Aktuelle Logs abfragen
END=$(date +%s)000000000
START=$(( $(date +%s) - 3600 ))000000000
curl -s "http://127.0.0.1:3100/loki/api/v1/query_range" \
--data-urlencode "query={unit=\"sshd.service\"} |= \"Failed password\"" \
--data-urlencode "start=$START" \
--data-urlencode "end=$END" \
--data-urlencode "limit=50" | jq .
Die Parameter start und end verwenden Unix-Zeitstempel in Nanosekunden. Die Shell-Berechnung oben ermittelt „jetzt minus 1 Stunde" und hängt neun Nullen für Nanosekunden-Präzision an.
Die Antwort ist JSON:
{
"status": "success",
"data": {
"resultType": "streams",
"result": [
{
"stream": {"unit": "sshd.service", "severity": "info"},
"values": [
["1710850000000000000", "Mar 19 14:00:00 vps sshd[1234]: Failed password for root from 203.0.113.5 port 22"]
]
}
]
}
}
Jeder Wert ist ein [Zeitstempel_Nanosekunden, Log_Zeile]-Paar. Das ist genau das Format, das Sie parsen, wenn Sie Logs an ein lokales LLM zur Analyse übergeben.
Labels und Streams abfragen
Alle Label-Namen auflisten:
curl -s "http://127.0.0.1:3100/loki/api/v1/labels" | jq .
Werte für ein bestimmtes Label auflisten:
curl -s "http://127.0.0.1:3100/loki/api/v1/label/unit/values" | jq .
Diese Endpunkte sind nützlich, um dynamische Abfragen in Automatisierungsskripten zu erstellen. Sie können alle Units aufzählen und dann jede einzelne nach Fehlern abfragen.
Sofort-Abfragen
Für Abfragen „genau jetzt" ohne Zeitbereich verwenden Sie /loki/api/v1/query:
curl -s "http://127.0.0.1:3100/loki/api/v1/query" \
--data-urlencode 'query=count_over_time({job="systemd-journal"} |= "error" [1h])' | jq .
Das gibt einen einzelnen Datenpunkt zurück: die Anzahl der Journal-Fehler in der letzten Stunde. Nützlich für Health-Checks und Monitoring-Skripte.
Fehlerbehebung
Promtail zeigt „permission denied" für den Docker-Socket:
Der Promtail-Container benötigt Lesezugriff auf /var/run/docker.sock. Prüfen Sie die Socket-Berechtigungen auf dem Host:
ls -la /var/run/docker.sock
Der Socket gehört typischerweise root:docker. Das Promtail-Image läuft standardmäßig als root, daher funktioniert das normalerweise. Wenn Sie Promtail mit einem benutzerdefinierten Benutzer ausführen, muss dieser in der Gruppe docker sein.
Keine Journal-Logs sichtbar:
Prüfen Sie zunächst die Promtail-Logs auf die Meldung support for reading the systemd journal is not compiled into this build of promtail. Wenn Sie diese sehen, unterstützt das Docker-Image kein Journal-Scraping. Installieren Sie Promtail als Host-Binary oder wechseln Sie zu Grafana Alloy (siehe Abschnitte oben).
Falls Promtail als Host-Binary mit Journal-Unterstützung läuft, prüfen Sie, ob das Journal-Verzeichnis existiert:
ls -la /var/log/journal/
Falls es nicht existiert, verwendet systemd flüchtigen (nur im Speicher) Journal-Speicher. Aktivieren Sie persistenten Speicher:
sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald
Aktualisieren Sie dann den Promtail Volume-Mount in docker-compose.yml von /run/log/journal auf /var/log/journal und starten Sie neu:
docker compose up -d promtail
Loki meldet „too many outstanding requests":
Die Abfragelast übersteigt Lokis Kapazität. Reduzieren Sie den Zeitbereich Ihrer Abfragen oder fügen Sie Abfrage-Limits hinzu:
limits_config:
max_query_parallelism: 16
max_query_series: 500
Grafana zeigt „Data source connected, no labels found":
Loki benötigt einige Minuten, um die ersten Logs aufzunehmen und zu indexieren. Warten Sie 2-3 Minuten und versuchen Sie die Abfrage erneut. Prüfen Sie, ob Loki bereit ist:
curl -s http://127.0.0.1:3100/ready
Alle Service-Logs gleichzeitig prüfen:
docker compose logs -f --tail=50
Das zeigt alle drei Dienste. Filtern Sie nach Problemen:
docker compose logs --tail=100 | grep -i error
Nächste Schritte
Ihre Loki-Pipeline sammelt Logs von systemd, Docker und Nginx. Sie können sie mit LogQL in Grafana oder über die HTTP-API abfragen.
Von hier aus:
- KI-Log-Analyse mit Ollama auf einem VPS: Anomalien mit einem lokalen LLM erkennen Übergeben Sie diese Logs an ein lokales LLM zur Anomalieerkennung und automatisierten Alarmierung
- AIOps auf einem VPS: KI-gesteuerte Serververwaltung mit Open-Source-Tools Überblick über den selbst gehosteten AIOps-Stack
- SigNoz oder OpenObserve auf einem VPS selbst hosten: Datadog-Alternativen im Vergleich Falls Sie ein All-in-One-APM dem Grafana-Stack vorziehen
Bereit, es selbst auszuprobieren?
Stellen Sie Ihren eigenen Server in Sekunden bereit. Linux, Windows oder FreeBSD. →