When a project runs on three to five VDS instances, finding a specific line in the logs turns into visiting each server over SSH one by one. Grafana Loki collects logs from all servers in one place and lets you search them through the same Grafana interface already used for metrics. Let's cover installing Loki, the Promtail agent, and typical LogQL queries.
Why centralized logging matters and how Loki differs from ELK
Elasticsearch indexes the full text of every log line, so an ELK cluster needs a lot of memory and disk even at moderate volumes. Loki works differently: it indexes only labels — server name, service name, log level — and stores the text itself compressed in blocks. Searching by labels is nearly instant, full-text search inside the matched blocks is a bit slower, but the infrastructure for Loki needs far fewer resources than Elasticsearch.
For a fleet of several VDS instances with ordinary load, that's a reasonable trade-off: no need for a separate three-node cluster just for logs.
Installing Loki on a VDS with Docker Compose
A minimal stack — Loki for storage, Promtail for collection, Grafana for viewing — comes up with one file:
services:
loki:
image: grafana/loki:3.1.0
ports:
- "3100:3100"
volumes:
- ./loki-data:/loki
command: -config.file=/etc/loki/local-config.yaml
grafana:
image: grafana/grafana:11.1.0
ports:
- "3000:3000"
volumes:
- ./grafana-data:/var/lib/grafana
After docker compose up -d, Loki accepts data on port 3100, and Grafana is available on port 3000 for setting up dashboards and queries.
Promtail: collecting logs from servers and shipping to Loki
Promtail is installed on every server that needs its logs collected, reads files from /var/log and docker containers, adds labels, and ships batches to Loki. The agent config:
server:
http_listen_port: 9080
clients:
- url: http://loki.internal:3100/loki/api/v1/push
scrape_configs:
- job_name: nginx
static_configs:
- targets: ["localhost"]
labels:
job: nginx
host: web-01
__path__: /var/log/nginx/*.log
The host label here is critical: without it, in a shared log stream from five servers there's no way to tell where a specific line came from.
Setting up labels for fast search
In Loki, it's important not to turn frequently changing values into labels — for example, a request ID or a timestamp inside the line. Every unique label value creates a separate stream, and thousands of unique values turn Loki into something like Elasticsearch in terms of index load.
- Labels should hold stable values: server name, service name, environment (prod/staging)
- A request ID, email, or IP address is searched with a full-text query inside an already selected stream, not through a label
- Keep the number of unique label combinations within a few hundred per installation
Connecting Loki as a data source in Grafana
In Grafana, the data source is added under Connections → Data sources → Loki, with the URL set to http://loki:3100 on a shared Docker network. Once saved, logs are searched from the Explore tab, the same interface already used for Prometheus graphs.
LogQL queries: examples
LogQL combines picking a stream by labels in curly braces with a text filter after the pipe character:
{host="web-01", job="nginx"} |= "500"
{job="nginx"} | json | status >= 500
sum(rate({job="nginx"} |= "500" [5m])) by (host)
The first query looks for the string "500" in nginx logs on a specific server, the second parses a JSON log and filters on the numeric status field, and the third counts the rate of 500 errors per minute for each server — useful for alerts and dashboards.
Storage and log rotation
By default Loki stores logs locally on disk with no time limit, so retention is configured separately with the retention_period parameter in the config, otherwise the server's disk fills up sooner or later. For a long-term log archive, fresh blocks can be shipped off to S3-compatible storage — see the article comparing S3 providers for details.
The Loki plus Promtail plus Grafana stack makes sense to deploy next to an already configured Uptime Kuma monitoring setup: availability metrics and error logs complement each other when investigating an incident. If there are many servers joined into a Docker Swarm, Promtail is deployed as a global service on every node of the cluster.
Summary: Loki launch checklist
Centralized logs save hours of searching during incidents, provided the stack is set up correctly from the start rather than thrown together after the first outage.
- Loki for storage, Promtail on every server, Grafana for queries
- The
hostlabel is mandatory on every log source - Labels hold only stable values, everything else stays in the log text
- The retention parameter is set explicitly, not left at the default