Gecentraliseerd logbeheer met Grafana Loki op een VPS

19 min leestijd·Matthieu·monitoringlogqlloggingdocker-composepromtaillokigrafana|

Deploy Grafana Loki, Promtail en Grafana via Docker Compose op een enkele VPS. Verzamel systemd-, Docker- en Nginx-logs, bevraag ze met LogQL en configureer retentie voor productie.

Verspreide logs in /var/log en docker logs-uitvoer worden onbeheersbaar zodra je meer dan twee services draait. Deze tutorial deployt de Grafana + Loki + Promtail stack op een enkele VPS met Docker Compose. Je verzamelt logs van systemd, Docker-containers en Nginx, bevraagt ze met LogQL, configureert retentie zodat logs je schijf niet vullen, en beveiligt de stack voor een publiek toegankelijke server.

Aan het einde is Loki's HTTP API klaar voor programmatische queries. Het artikel AI-loganalyse met Ollama op een VPS: anomalieën detecteren met een lokaal LLM bouwt hierop voort om logs naar een lokaal LLM te sturen voor anomaliedetectie.

Wat doet de Grafana + Loki + Promtail stack?

Grafana Loki is een open-source log-aggregatiesysteem dat alleen log-labels (metadata) indexeert, niet de volledige tekst van logregels. Dit maakt het veel lichter qua resources dan Elasticsearch. Loki slaat gecomprimeerde log-chunks op in het bestandssysteem of objectopslag. Samen met Promtail voor verzameling en Grafana voor visualisatie vormt het een complete gecentraliseerde logging stack.

De drie componenten hebben verschillende rollen:

Component Rol Resourcegebruik
Loki Ontvangt, slaat op en indexeert logs. Beantwoordt queries. 300-600 MB RAM in rust, tot 1 GB bij zware queries
Promtail Ontdekt logbronnen, leest bestanden continu, stuurt entries naar Loki 50-100 MB RAM
Grafana Webinterface voor het bevragen en visualiseren van logs via Explore 200-300 MB RAM

Totaal stackgebruik: 1-1,5 GB RAM. Een 2 GB VPS is het minimum. Een 4 GB VPS geeft comfortabele ruimte voor LogQL-queries op grotere datasets.

Loki vs Elasticsearch: Elasticsearch indexeert elk woord in elke logregel, wat full-text zoeken geeft maar 10-20x meer RAM en schijfruimte kost. Loki's label-gebaseerde index betekent dat je eerst op labels filtert en dan door de overeenkomende chunks zoekt. Voor de meeste VPS-workloads is dit de juiste afweging. Als je full-text zoeken over terabytes aan logs nodig hebt, is Loki niet het juiste gereedschap.

Vereisten

  • Een VPS met minimaal 2 GB RAM (4 GB aanbevolen). Een Virtua Cloud VPS met 4 vCPU en 8 GB RAM draait deze stack zonder problemen.
  • Docker en Docker Compose geïnstalleerd. Als je hulp nodig hebt bij de installatie, zie [-> docker-compose-multi-service-vps].
  • Een niet-root gebruiker met sudo-toegang.
  • Basiskennis van de terminal en YAML-syntax.

Controleer of Docker draait:

docker --version
docker compose version

Je zou Docker 24+ en Compose v2+ moeten zien. Als een van de commando's faalt, is Docker niet geïnstalleerd of ontbreekt de Compose-plugin.

Hoe deploy ik Loki met Docker Compose op een VPS?

Maak een projectdirectory en drie configuratiebestanden: docker-compose.yml, loki-config.yml en promtail-config.yml. Het Docker Compose-bestand pint alle images op specifieke versies, stelt resourcelimieten in, configureert persistente volumes en bindt Loki alleen aan localhost.

Projectstructuur

mkdir -p ~/loki-stack/{loki-data,promtail-data}
cd ~/loki-stack

De loki-data directory bevat chunks, indexen en het write-ahead log. De promtail-data directory slaat Promtails positiebestand op zodat het na herstarts kan hervatten.

Stel het eigenaarschap in op de Loki-datadirectory. De Loki 3.x Docker-image draait als UID 10001, niet als root. Zonder dit faalt Loki bij het starten met "permission denied" bij het aanmaken van subdirectories:

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

Let op: Loki en Grafana luisteren op 127.0.0.1, niet op 0.0.0.0. Dit voorkomt externe toegang. Je benadert Grafana via een SSH-tunnel of een reverse proxy. Loki of Grafana direct op internet zetten is een veelgemaakte fout die in de meeste online tutorials voorkomt.

Het restart: unless-stopped beleid zorgt ervoor dat elke service herstarts overleeft. Als je handmatig een service stopt met docker compose stop, blijft die gestopt. Anders herstart hij automatisch.

Gepinde image-tags (3.6.7, 11.5.2) voorkomen onverwachte upgrades. Gebruik nooit :latest in productie. Wanneer je wilt upgraden, wijzig de tag en voer docker compose up -d uit om de nieuwe image te downloaden.

Grafana-adminwachtwoord genereren

Gebruik nooit standaard inloggegevens. Genereer een sterk wachtwoord en sla het op in een secrets-bestand met beperkte rechten:

mkdir -p ~/loki-stack/secrets
openssl rand -base64 32 > ~/loki-stack/secrets/grafana_admin_pw
chmod 644 ~/loki-stack/secrets/grafana_admin_pw

Controleer de rechten:

ls -la ~/loki-stack/secrets/grafana_admin_pw

Je zou -rw-r--r-- moeten zien. Het bestand moet voor iedereen leesbaar zijn omdat Docker Compose (buiten Swarm-modus) bestandsgebaseerde secrets mount met de rechten van het bronbestand. Grafana draait als UID 472 in de container en heeft leestoegang nodig. Het bestand wordt nog steeds beschermd door zijn locatie in een specifieke secrets-directory, en alleen de root-gebruiker van de host kan het wijzigen. De omgevingsvariabele GF_SECURITY_ADMIN_PASSWORD__FILE vertelt Grafana om het wachtwoord bij het opstarten uit dit bestand te lezen in plaats van het in het compose-bestand in te sluiten.

loki-config.yml

Deze configuratie gebruikt TSDB met schema v13 (Loki 3.x standaarden), bestandssysteemopslag voor single-node deployment en de compactor voor retentie:

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

Belangrijke keuzes in deze configuratie:

  • auth_enabled: false is hier veilig omdat Loki alleen op localhost luistert (Docker intern netwerk + 127.0.0.1 poortbinding). Multi-tenant setups vereisen auth_enabled: true met een X-Scope-OrgID header bij elk verzoek.
  • retention_period: 720h bewaart logs gedurende 30 dagen. Loki 3.x gebruikt standaard 0s (voor altijd bewaren) als je dit niet instelt. Je schijf zal vollopen.
  • schema: v13 met store: tsdb is vereist voor Loki 3.x functionaliteit. Oudere boltdb-shipper configuraties uit Loki 2.x tutorials zullen niet starten of deprecation-waarschuwingen geven.
  • chunk_encoding: snappy comprimeert chunks met Snappy. Sneller dan gzip, iets grotere bestanden. Goede standaard voor single-node waar CPU meer beperkt is dan schijfruimte.
  • WAL ingeschakeld: het write-ahead log beschermt tegen dataverlies als Loki crasht tijdens het schrijven. Bij herstart speelt Loki het WAL af om niet-bevestigde entries te herstellen. Je ziet "WAL replay" berichten in de logs bij het opstarten. Dit is normaal.
  • max_label_names_per_series: 15 komt overeen met de Loki 3.x standaard. Houd de label-kardinaliteit laag. Labels als user_id of request_id creëren te veel streams en verslechteren de prestaties.

Hoe configureer ik Promtail voor systemd journal logs?

Promtail verzamelt logs uit meerdere bronnen en stuurt ze naar Loki. De onderstaande configuratie verzamelt uit drie bronnen in aparte scrape_configs jobs: systemd journal, Docker-containers en Nginx-logbestanden.

Promtail end-of-life melding: Promtail heeft op 2 maart 2026 zijn end-of-life bereikt. Grafana Alloy is de officiële opvolger. Deze tutorial gebruikt Promtail omdat de configuratieconcepten direct vertalen naar Alloy, en miljoenen deployments het nog gebruiken. Zie de sectie "Is Promtail verouderd?" hieronder voor migratiestappen.

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

Het positions.yaml bestand houdt bij hoever Promtail heeft gelezen in elke logbron. Als Promtail herstart, hervat het waar het gebleven was in plaats van oude logs opnieuw te versturen of nieuwe te missen.

Hoe de journal-job werkt

Het journal blok leest direct uit het systemd journal via de journal API. De instelling max_age: 12h vertelt Promtail om bij de eerste start alleen journal-entries van de laatste 12 uur op te nemen. Zonder dit zou Promtail proberen de volledige journal-historie op te nemen, die op lang draaiende servers gigabytes kan zijn.

De relabel_configs extraheren metadata uit journal-entries naar Loki-labels. __journal__systemd_unit wordt het label unit (bijv. sshd.service, nginx.service). __journal_priority_keyword wordt severity (bijv. warning, err, info). Deze labels stellen je in staat om efficiënt te filteren in LogQL zonder elke regel te scannen.

Voor journal-scraping heeft de Promtail-container twee volume-mounts nodig: /run/log/journal (of /var/log/journal als je systeem logs persistent opslaat) en /etc/machine-id. De machine-ID identificeert welk journal gelezen moet worden.

Docker image beperking: De standaard grafana/promtail Docker image is niet gecompileerd met systemd journal-ondersteuning. Als je support for reading the systemd journal is not compiled into this build of promtail in de logs ziet, werkt journal-scraping niet vanuit de Docker-container. Je hebt twee opties:

  1. Installeer Promtail als host-binary in plaats van de Docker image te gebruiken. Download het van de Loki releases pagina en voer het direct uit op de host, waar het native toegang heeft tot de journal API.
  2. Gebruik Grafana Alloy (zie de migratiesectie hieronder), dat journal-scraping ondersteunt in zijn Docker image.

De Docker en Nginx bestand-scraping jobs werken prima in de Docker image. Alleen journal-scraping vereist de host-binary of Alloy.

Hoe verzamel ik Docker-container logs met Promtail?

De docker job gebruikt Docker service discovery om automatisch alle draaiende containers op de host te ontdekken. Promtail maakt verbinding met de Docker daemon via /var/run/docker.sock en pollt elke 5 seconden naar nieuwe containers. Wanneer een container start of stopt, begint of stopt Promtail automatisch met het lezen van zijn logs.

De relabel_configs extraheren nuttige metadata:

  • __meta_docker_container_name wordt het label container. De regex '/(.+)' verwijdert de voorloop / die Docker aan containernamen toevoegt.
  • __meta_docker_container_log_stream wordt het label stream (stdout of stderr).
  • __meta_docker_container_label_com_docker_compose_service extraheert de Compose-servicenaam (bijv. loki, grafana). Dit label bestaat alleen voor containers beheerd door Docker Compose.

Je hoeft geen individuele containers te configureren. Elke container met een log-driver die naar het bestandssysteem schrijft (de standaard json-file driver) wordt ontdekt. Als je een databasecontainer, een webapp en een cache draait, verschijnen alle drie automatisch in Loki onder hun containernamen.

Om specifieke containers uit te sluiten van logverzameling, voeg een Docker-label toe en filter daarop:

    relabel_configs:
      # ... existing relabel rules ...
      - source_labels: ['__meta_docker_container_label_logging']
        regex: 'disabled'
        action: drop

Dan op de container die je wilt uitsluiten:

  noisy-service:
    image: some/image
    labels:
      logging: "disabled"

Hoe scrape ik Nginx access en error logs met Promtail?

De nginx job gebruikt static_configs met het label __path__ om specifieke logbestanden te volgen. In tegenstelling tot Docker service discovery moet je de logbestandspaden van tevoren kennen. Nginx schrijft standaard naar /var/log/nginx/access.log en /var/log/nginx/error.log.

Het type label onderscheidt access- en errorlogs. Dit stelt je in staat ze apart te bevragen in LogQL:

{job="nginx", type="access"}    # alleen access logs
{job="nginx", type="error"}     # alleen error logs
{job="nginx"}                   # beide

Als Nginx niet op de host is geïnstalleerd, logt Promtail een waarschuwing over ontbrekende bestanden maar gaat door met het verzamelen uit andere bronnen. Dit is onschadelijk. Verwijder de nginx-job uit de configuratie als je Nginx niet gebruikt.

Voor Nginx in een Docker-container heb je twee opties. Je kunt Docker service discovery gebruiken (stdout/stderr van de container worden automatisch vastgelegd). Of je kunt de Nginx-logdirectory mounten als gedeeld volume en de statische bestandsscraper gebruiken. De Docker-aanpak is eenvoudiger. De bestandsaanpak geeft je aparte labels type: access en type: error.

Stack starten

cd ~/loki-stack
docker compose up -d

Controleer of alle drie containers draaien:

docker compose ps

Je zou loki, promtail en grafana moeten zien met status Up. Als een service Restarting toont, bekijk de logs:

docker compose logs <service-name> --tail=30

Controleer of Loki gereed is:

curl -s http://127.0.0.1:3100/ready

Verwachte uitvoer: ready. Als je Ingester not ready: waiting for 15s after being ready krijgt, wacht 15 seconden en probeer opnieuw. Loki heeft tijd nodig om de ingester ring te initialiseren.

Controleer de Promtail targets:

docker compose logs promtail --tail=20

Zoek naar regels die ontdekte targets tonen. Je zou entries moeten zien voor het journal, de Docker socket en de Nginx-logpaden. Er zouden geen level=error regels moeten verschijnen.

Hoe verifieer ik dat logs verschijnen in Grafana Explore?

Open een SSH-tunnel om Grafana te benaderen vanaf je lokale machine. We gebruiken een tunnel omdat Grafana op de VPS aan localhost gebonden is en niet blootgesteld aan internet.

ssh -L 3000:127.0.0.1:3000 user@your-vps-ip

Open http://localhost:3000 in je browser. Log in met gebruikersnaam admin en het wachtwoord uit ~/loki-stack/secrets/grafana_admin_pw. Lees het met:

cat ~/loki-stack/secrets/grafana_admin_pw

Loki als databron toevoegen

  1. Ga naar Connections > Data Sources > Add data source
  2. Selecteer Loki
  3. Stel de URL in op http://loki:3100 (dit is de Docker interne hostnaam, niet localhost)
  4. Klik op Save & test

Je zou "Data source successfully connected." moeten zien. Als het faalt, controleer of beide containers op hetzelfde Docker-netwerk (loki-net) zitten.

Je eerste query uitvoeren

  1. Ga naar Explore (kompas-icoon in de zijbalk)
  2. Selecteer de Loki databron
  3. Schakel naar Code modus (niet Builder) en voer deze LogQL query in:
{job="systemd-journal"} |= "ssh"

Dit toont alle systemd journal entries die "ssh" bevatten. Als je logregels ziet, werkt de volledige pipeline: journal -> Promtail -> Loki -> Grafana.

Probeer een Docker container query:

{compose_service="loki"}

Dit geeft Loki's eigen logs terug, verzameld door Promtail via Docker service discovery.

En een Nginx query (als Nginx geïnstalleerd is en logs genereert):

{job="nginx", type="error"}

Als Grafana "No data" toont, wacht 2-3 minuten. Loki heeft tijd nodig om de eerste batch logs op te nemen en te indexeren.

Wat zijn de nuttigste LogQL queries voor serverlogs?

LogQL heeft twee querytypes: logqueries geven logregels terug, metriekqueries geven numerieke waarden terug. Beide beginnen met een stream selector ({label="value"}) die kiest welke logstreams gescand worden, en voegen dan filters en parsers toe om resultaten te verfijnen.

Stream selectors en regelfilters

# Alle logs van de SSH daemon
{unit="sshd.service"}

# Regels met "Failed password"
{unit="sshd.service"} |= "Failed password"

# Regels ZONDER "Accepted"
{unit="sshd.service"} != "Accepted"

# Regex match: IP-adressen
{unit="sshd.service"} |~ "\\d+\\.\\d+\\.\\d+\\.\\d+"

# Hoofdletterongevoelig zoeken
{job="nginx", type="error"} |~ "(?i)timeout"

Regelfilters (|=, !=, |~, !~) draaien na de stream selector. Ze doorzoeken de inhoud van logregels. Meerdere filters worden gekoppeld en moeten allemaal matchen:

{unit="sshd.service"} |= "Failed" |= "root"

Dit vindt regels die zowel "Failed" als "root" bevatten.

Parsers

Parsers extraheren gestructureerde velden uit ongestructureerde logregels. Na het parsen kun je filteren op geëxtraheerde velden zoals status >= 500 in plaats van regex te gebruiken. Kies de juiste parser voor je logformaat:

Parser Syntax Geschikt voor Prestatienotities
logfmt | logfmt Key=value logs (systemd, Go apps) Snelste. Geen regex.
json | json JSON-gestructureerde logs Snel. Native JSON parsing.
pattern | pattern "<pattern>" Logs met vast formaat (Nginx combined, Apache) Snel. Positionele extractie.
regexp | regexp "<regex>" Onregelmatige formaten, gemengde structuren Langzaamste. Gebruik als laatste redmiddel.

Gebruik logfmt of json wanneer je logs al structuur hebben. Gebruik pattern voor bekende formaten zoals het Nginx combined log. Gebruik regexp alleen wanneer niets anders werkt, want regex-parsing is aanzienlijk trager op streams met hoog volume.

Nginx access log met pattern parser:

{job="nginx", type="access"}
  | pattern "<ip> - - [<timestamp>] \"<method> <uri> <_>\" <status> <bytes>"
  | status >= 500

Dit parst het Nginx combined log formaat in benoemde velden en filtert op 5xx fouten. De <_> placeholder verwerpt velden die je niet nodig hebt (de HTTP-versie in dit geval).

JSON log parser:

{compose_service="myapp"}
  | json
  | level="error"
  | line_format "{{.timestamp}} {{.message}}"

De line_format stage herformatteert de uitvoer. Handig wanneer JSON-logs rommelig zijn en je schonere uitvoer wilt in Grafana.

Metrieken uit logs

Metriekqueries zetten logregels om in getallen. Ze voeden Grafana dashboards en alerting-regels:

# Mislukte SSH login rate per minuut over het afgelopen uur
rate({unit="sshd.service"} |= "Failed password" [5m])

# Totaal Nginx 5xx fouten in 5-minuten vensters
count_over_time(
  {job="nginx", type="access"}
    | pattern "<_> - - [<_>] \"<_> <_> <_>\" <status> <_>"
    | status >= 500
  [5m]
)

# P95 responstijd uit JSON app logs
# unwrap extraheert een numeriek veld voor aggregatie
quantile_over_time(0.95,
  {compose_service="myapp"}
    | json
    | unwrap response_time_ms
    | __error__=""
  [5m]
) by (endpoint)

# Bytes per seconde geserveerd door Nginx
sum(rate(
  {job="nginx", type="access"}
    | pattern "<_> - - [<_>] \"<_> <_> <_>\" <_> <bytes>"
    | unwrap bytes
    | __error__=""
  [5m]
))

Het | __error__="" filter na unwrap verwijdert regels waar de numerieke extractie faalde (niet-numerieke waarden, ontbrekende velden). Zonder dit produceren die regels stilletjes nulwaarden en scheeftrekken je resultaten. Voeg dit filter altijd toe na unwrap.

De [5m] range definieert de venstergrootte. Kortere ranges (1m) geven meer gedetailleerde maar ruisiger data. Langere ranges (15m, 1h) vlakken pieken af. Voor dashboards is 5m een goed startpunt.

Hoe stel ik logretentie in Loki 3.x in?

In Loki 3.x wordt retentie beheerd door de compactor. Stel retention_period in onder limits_config en schakel de compactor in met retention_enabled: true. De standaard retentie in Loki 3.0+ is 0s (voor altijd bewaren), dus je moet dit expliciet configureren of je schijf loopt vol.

De loki-config.yml hierboven bevat al retentie. Zo werken de instellingen samen:

compactor:
  retention_enabled: true        # Moet true zijn, anders compacteert de compactor alleen (geen verwijdering)
  retention_delete_delay: 2h     # 2u wachten na markering van chunks voor verwijdering
  compaction_interval: 10m       # Hoe vaak de compactor draait

limits_config:
  retention_period: 720h         # 30 dagen als globale standaard

De compactor draait als onderdeel van het Loki-proces in single-node modus. Het scant de TSDB-index, identificeert chunks ouder dan de retentieperiode, markeert ze voor verwijdering en verwijdert ze na de retention_delete_delay. De vertraging geeft je een herstellvenster bij verkeerde configuratie.

Retentie per stream

Je kunt verschillende retentieperioden instellen voor verschillende logstreams. Logs met hoog volume en lage waarde (zoals debug-uitvoer) kunnen sneller verlopen:

limits_config:
  retention_period: 720h
  retention_stream:
    - selector: '{job="nginx", type="access"}'
      priority: 1
      period: 336h    # 14 dagen voor access logs
    - selector: '{severity="debug"}'
      priority: 2
      period: 72h     # 3 dagen voor debug logs

Hogere prioriteitswaarden winnen wanneer meerdere selectors dezelfde stream matchen. Een debug-level Nginx access log matcht beide regels. Prioriteit 2 wint, dus het krijgt 3 dagen retentie.

Retentie dimensionering

Schat het schijfgebruik voordat je je vastlegt op een retentieperiode. Loki comprimeert logs goed (5-10x ratio met Snappy), maar de getallen tellen op bij drukke servers:

Ruw logvolume/dag Gecomprimeerd (geschat) 7-dagen retentie 30-dagen retentie 90-dagen retentie
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

Dit zijn schattingen. Werkelijke compressie hangt af van de loginhoud. Repetitieve logs (access logs met vergelijkbare paden) comprimeren beter dan willekeurige debug-uitvoer. Monitor het werkelijke gebruik met:

du -sh ~/loki-stack/loki-data/

Voer dit wekelijks uit om onverwachte groei te detecteren voordat je zonder schijfruimte zit.

Hoe optimaliseer ik Loki voor productie op een enkele VPS?

Een standaard Loki-configuratie werkt voor testen maar heeft tuning nodig voor een productie-VPS. De onderstaande wijzigingen verminderen geheugenpieken, beschermen tegen ongecontroleerde logstreams en verharden de stack voor een publiek toegankelijke server.

Loki aan localhost binden

Al gedaan in de docker-compose.yml hierboven (127.0.0.1:3100:3100). Dubbelcheck na deployment:

ss -tlnp | grep 3100

Je zou 127.0.0.1:3100 moeten zien, niet 0.0.0.0:3100. Doe hetzelfde voor Grafana op poort 3000.

Firewallregels

Als je ufw gebruikt, blokkeer externe toegang tot de logging-poorten:

sudo ufw deny 3100/tcp comment "Loki - localhost only"
sudo ufw deny 3000/tcp comment "Grafana - localhost only"
sudo ufw status numbered

Aangezien de poorten al aan localhost gebonden zijn, is de firewall een extra beveiligingslaag (defense in depth). Als iemand per ongeluk het compose-bestand wijzigt om aan 0.0.0.0 te binden, blokkeert de firewall nog steeds externe toegang.

Versie-informatie verbergen

Versie-openbaarmaking helpt aanvallers bekende kwetsbaarheden te targeten. GF_ANALYTICS_REPORTING_ENABLED=false in het compose-bestand schakelt al Grafana-telemetrie uit. Loki's /loki/api/v1/status/buildinfo endpoint geeft versiedetails prijs, maar aangezien Loki aan localhost gebonden is, kunnen alleen lokale processen het bereiken.

Als je Grafana achter een reverse proxy (Nginx, Caddy) plaatst, voeg deze Nginx-instellingen toe:

server_tokens off;
proxy_hide_header X-Powered-By;

Belangrijke breaking changes in Loki 3.x

Als je migreert van een Loki 2.x configuratie of een ouder tutorial volgt, let op deze wijzigingen:

Wijziging Loki 2.x standaard Loki 3.x standaard Vereiste actie
Schema v11/v12 v13 Gebruik schema: v13 met store: tsdb
Index store boltdb-shipper tsdb Migreer naar TSDB (BoltDB verouderd)
Retentie 0s (voor altijd bewaren) 0s (voor altijd bewaren) Stel retention_period expliciet in
Gestructureerde metadata Uitgeschakeld Ingeschakeld Vereist v13 schema
Max labels per serie 30 15 Verminder label-kardinaliteit of verhoog limiet
Docker image Bevat BusyBox shell Geen shell Kan geen docker exec in container uitvoeren

De shell-loze Docker image in Loki 3.6+ betekent dat je docker exec -it loki sh niet kunt gebruiken voor debugging. Controleer in plaats daarvan logs met docker compose logs loki en gereedheid met curl http://127.0.0.1:3100/ready.

Bestandssysteem vs objectopslag

Voor een enkele VPS is bestandssysteemopslag de juiste keuze. Objectopslag (S3, GCS, MinIO) voegt complexiteit en latentie toe die alleen loont wanneer je nodig hebt:

  • Meerdere Loki-instanties die dezelfde data delen
  • Onbeperkte opslag voorbij je VPS-schijf
  • Cross-regio replicatie

Blijf bij bestandssysteemopslag totdat je een enkel node ontgroeit.

De stack monitoren

Controleer het resourcegebruik van draaiende containers:

docker stats --no-stream

Controleer Loki-logs op waarschuwingen en fouten:

docker compose logs loki --tail=50 | grep -E "level=(error|warn)"

Veelvoorkomende problemen en oplossingen:

  • stream limit exceeded: verhoog max_streams_per_user in limits_config. Meestal veroorzaakt door labels met hoge kardinaliteit.
  • ingestion rate limit reached: verhoog ingestion_rate_mb. Komt voor bij log-bursts (deployments, fout-stormen).
  • WAL replay: normaal bij het opstarten. Loki herstelt niet-bevestigde schrijfacties uit het write-ahead log.
  • Hoog geheugengebruik: verminder max_size_mb in chunk_cache_config of verlaag ingestion_burst_size_mb.

Is Promtail verouderd? Moet ik Grafana Alloy gebruiken?

Ja. Promtail ging op 13 februari 2025 in Long-Term Support en bereikte op 2 maart 2026 zijn end-of-life. Er worden geen toekomstige updates, bugfixes of beveiligingspatches meer uitgebracht. Grafana Alloy is de officiële vervanger. Het is Grafana Labs' distributie van de OpenTelemetry Collector en verwerkt logs, metrieken, traces en profiling-data in een enkele agent.

Waarom deze tutorial nog Promtail gebruikt

De configuratieconcepten van Promtail vertalen direct naar Alloy. De scrape configs, relabel-regels en pipeline-stages werken hetzelfde. Promtail leren is nog steeds nuttig omdat:

  1. Miljoenen bestaande deployments het gebruiken
  2. De Alloy migratietool Promtail-configs automatisch converteert
  3. Promtail begrijpen het debuggen van Alloy-configuraties makkelijker maakt
  4. Het zoekvolume voor Promtail-tutorials hoog blijft en de concepten overdraagbaar zijn

Migreren naar Alloy

Converteer je Promtail-configuratie naar Alloy-formaat met één commando:

alloy convert --source-format=promtail --output=alloy-config.alloy promtail-config.yml

Dit genereert een configuratiebestand in Alloy-formaat. Controleer de uitvoer voor deployment. De converter verwerkt de meeste gevallen maar kan handmatige aanpassingen vereisen voor aangepaste pipeline-stages.

Vervang dan de Promtail-service 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

Verwijder het promtail serviceblok en voer docker compose up -d uit. Alloy begint dezelfde logbronnen te verzamelen met de geconverteerde configuratie.

Voor nieuwe deployments vanaf nul: gebruik Alloy vanaf het begin. Voor bestaande Promtail-setups: plan de migratie maar er is geen onmiddellijke urgentie. De Promtail-binary blijft werken. Pin de image tag (grafana/promtail:3.6.7) zodat je controle houdt over wat draait.

Hoe bevraag ik Loki logs programmatisch via de HTTP API?

Loki biedt een REST API voor programmatische logqueries. Zo integreer je Loki met scripts, alerting-pipelines of de AI-loganalyselaag beschreven in AI-loganalyse met Ollama op een VPS: anomalieën detecteren met een lokaal LLM.

De API accepteert dezelfde LogQL-queries die je in Grafana gebruikt. Het hoofdendpoint is /loki/api/v1/query_range voor tijdsbereik-queries.

Recente logs bevragen

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 .

De start en end parameters gebruiken Unix nanoseconde-timestamps. De shell-berekening hierboven berekent "nu min 1 uur" en voegt negen nullen toe voor nanoseconde-precisie.

Het antwoord is 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"]
        ]
      }
    ]
  }
}

Elke waarde is een [timestamp_nanoseconden, logregel] paar. Dit is precies het formaat dat je parst wanneer je logs naar een lokaal LLM stuurt voor analyse.

Labels en streams bevragen

Alle labelnamen oplijsten:

curl -s "http://127.0.0.1:3100/loki/api/v1/labels" | jq .

Waarden voor een specifiek label oplijsten:

curl -s "http://127.0.0.1:3100/loki/api/v1/label/unit/values" | jq .

Deze endpoints zijn nuttig voor het bouwen van dynamische queries in automatiseringsscripts. Je kunt alle units opsommen en dan elke unit bevragen op fouten.

Instant queries

Voor "nu" queries zonder tijdsbereik, gebruik /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 .

Dit geeft een enkel datapunt terug: het aantal journal-fouten in het afgelopen uur. Handig voor health checks en monitoringscripts.

Probleemoplossing

Promtail toont "permission denied" voor Docker socket:

De Promtail-container heeft leestoegang nodig tot /var/run/docker.sock. Controleer de socketrechten op de host:

ls -la /var/run/docker.sock

De socket is typisch eigendom van root:docker. De Promtail image draait standaard als root, dus dit werkt normaal. Als je Promtail met een aangepaste gebruiker draait, moet die gebruiker in de docker groep zitten.

Geen journal logs verschijnen:

Controleer eerst de Promtail-logs op het bericht support for reading the systemd journal is not compiled into this build of promtail. Als je dit ziet, ondersteunt de Docker image geen journal-scraping. Installeer Promtail als host-binary of schakel over naar Grafana Alloy (zie secties hierboven).

Als Promtail als host-binary draait met journal-ondersteuning, controleer of de journal-directory bestaat:

ls -la /var/log/journal/

Als deze niet bestaat, gebruikt systemd vluchtige (alleen-geheugen) journal-opslag. Schakel persistente opslag in:

sudo mkdir -p /var/log/journal
sudo systemd-tmpfiles --create --prefix /var/log/journal
sudo systemctl restart systemd-journald

Werk dan de Promtail volume-mount in docker-compose.yml bij van /run/log/journal naar /var/log/journal en herstart:

docker compose up -d promtail

Loki rapporteert "too many outstanding requests":

De querybelasting overschrijdt Loki's capaciteit. Verminder het tijdsbereik van je queries of voeg query-limieten toe:

limits_config:
  max_query_parallelism: 16
  max_query_series: 500

Grafana toont "Data source connected, no labels found":

Loki heeft een paar minuten nodig om de eerste logs op te nemen en te indexeren. Wacht 2-3 minuten en probeer de query opnieuw. Controleer of Loki gereed is:

curl -s http://127.0.0.1:3100/ready

Alle servicelogs tegelijk controleren:

docker compose logs -f --tail=50

Dit volgt alle drie services. Filter op problemen:

docker compose logs --tail=100 | grep -i error

Volgende stappen

Je Loki-pipeline verzamelt logs van systemd, Docker en Nginx. Je kunt ze bevragen met LogQL in Grafana of via de HTTP API.

Vanaf hier:


Copyright 2026 Virtua.Cloud. Alle rechten voorbehouden. Deze inhoud is een origineel werk van het Virtua.Cloud-team. Reproductie, herpublicatie of herdistributie zonder schriftelijke toestemming is verboden.

Klaar om het zelf te proberen?

Deploy uw eigen server in seconden. Linux, Windows of FreeBSD.

Bekijk VPS-aanbod