Zentrales Log-Management mit Grafana Loki auf einem VPS

19 Min. Lesezeit·Matthieu·monitoringlogqlloggingdocker-composepromtaillokigrafana|

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: false ist hier sicher, da Loki nur auf localhost lauscht (Docker-internes Netzwerk + 127.0.0.1 Port-Binding). Multi-Tenant-Setups benötigen auth_enabled: true mit einem X-Scope-OrgID-Header bei jeder Anfrage.
  • retention_period: 720h behält Logs für 30 Tage. Loki 3.x verwendet standardmäßig 0s (unbegrenzt aufbewahren), wenn Sie diesen Wert nicht setzen. Ihre Festplatte wird volllaufen.
  • schema: v13 mit store: tsdb ist für Loki 3.x Funktionen erforderlich. Ältere boltdb-shipper-Konfigurationen aus Loki 2.x Tutorials werden beim Start fehlschlagen oder Deprecation-Warnungen ausgeben.
  • chunk_encoding: snappy komprimiert 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: 15 entspricht dem Loki 3.x Standard. Halten Sie die Label-Kardinalität niedrig. Labels wie user_id oder request_id erzeugen 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/promtail ist nicht mit systemd-Journal-Unterstützung kompiliert. Wenn Sie support for reading the systemd journal is not compiled into this build of promtail in den Logs sehen, funktioniert das Journal-Scraping nicht aus dem Docker-Container. Sie haben zwei Optionen:

  1. 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.
  2. 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_name wird zum Label container. Die Regex '/(.+)' entfernt den führenden /, den Docker an Containernamen anfügt.
  • __meta_docker_container_log_stream wird zum Label stream (stdout oder stderr).
  • __meta_docker_container_label_com_docker_compose_service extrahiert 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

  1. Gehen Sie zu Connections > Data Sources > Add data source
  2. Wählen Sie Loki
  3. Setzen Sie die URL auf http://loki:3100 (das ist der Docker-interne Hostname, nicht localhost)
  4. 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

  1. Gehen Sie zu Explore (Kompass-Symbol in der Seitenleiste)
  2. Wählen Sie die Loki-Datenquelle
  3. 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 Sie max_streams_per_user in limits_config. Meist durch Labels mit hoher Kardinalität verursacht.
  • ingestion rate limit reached: Erhöhen Sie ingestion_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_mb in chunk_cache_config oder verringern Sie ingestion_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:

  1. Millionen bestehender Deployments es verwenden
  2. Das Alloy-Migrationstool Promtail-Configs automatisch konvertiert
  3. Promtail-Verständnis das Debugging von Alloy-Konfigurationen erleichtert
  4. 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: