Gecentraliseerd logbeheer met Grafana Loki op een VPS
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: falseis hier veilig omdat Loki alleen op localhost luistert (Docker intern netwerk + 127.0.0.1 poortbinding). Multi-tenant setups vereisenauth_enabled: truemet eenX-Scope-OrgIDheader bij elk verzoek.retention_period: 720hbewaart logs gedurende 30 dagen. Loki 3.x gebruikt standaard0s(voor altijd bewaren) als je dit niet instelt. Je schijf zal vollopen.schema: v13metstore: tsdbis vereist voor Loki 3.x functionaliteit. Oudereboltdb-shipperconfiguraties uit Loki 2.x tutorials zullen niet starten of deprecation-waarschuwingen geven.chunk_encoding: snappycomprimeert 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: 15komt overeen met de Loki 3.x standaard. Houd de label-kardinaliteit laag. Labels alsuser_idofrequest_idcreë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/promtailDocker image is niet gecompileerd met systemd journal-ondersteuning. Als jesupport for reading the systemd journal is not compiled into this build of promtailin de logs ziet, werkt journal-scraping niet vanuit de Docker-container. Je hebt twee opties:
- 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.
- 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_namewordt het labelcontainer. De regex'/(.+)'verwijdert de voorloop/die Docker aan containernamen toevoegt.__meta_docker_container_log_streamwordt het labelstream(stdoutofstderr).__meta_docker_container_label_com_docker_compose_serviceextraheert 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
- Ga naar Connections > Data Sources > Add data source
- Selecteer Loki
- Stel de URL in op
http://loki:3100(dit is de Docker interne hostnaam, niet localhost) - 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
- Ga naar Explore (kompas-icoon in de zijbalk)
- Selecteer de Loki databron
- 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: verhoogmax_streams_per_userinlimits_config. Meestal veroorzaakt door labels met hoge kardinaliteit.ingestion rate limit reached: verhoogingestion_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_mbinchunk_cache_configof verlaagingestion_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:
- Miljoenen bestaande deployments het gebruiken
- De Alloy migratietool Promtail-configs automatisch converteert
- Promtail begrijpen het debuggen van Alloy-configuraties makkelijker maakt
- 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:
- AI-loganalyse met Ollama op een VPS: anomalieën detecteren met een lokaal LLM Stuur deze logs naar een lokaal LLM voor anomaliedetectie en geautomatiseerde alerting
- AIOps op een VPS: AI-gestuurde serverbeheer met open-source tools Overzicht van de zelf-gehoste AIOps-stack
- [-> self-host-signoz-openobserve-vps] Als je de voorkeur geeft aan een all-in-one APM boven de Grafana-stack
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