Aggregate Logs Without an ELK Stack
02 Oct 2026An ELK stack (Elasticsearch, Logstash, Kibana) is three JVM processes and a memory bill you don’t need for a handful of servers. Grafana Loki plus a single log-shipping agent does the same job for a small fleet — search, filter, tail — on a fraction of the resources.
Why not ELK for a small setup
Elasticsearch alone wants multiple GB of RAM to run comfortably, and that’s before Logstash and Kibana. Fine for a company with a dedicated logging cluster. Overkill for three VPSes and a couple of Docker hosts.
Loki takes a different approach: it doesn’t index full log text, only labels (like container, host, job). That keeps storage and memory small, and it’s the tradeoff that makes it viable on modest hardware.
The stack
Two pieces:
- Loki — stores and indexes logs, one binary.
- Promtail (or Grafana Alloy, its successor) — runs on each host, tails log files or the Docker socket, ships to Loki.
Grafana itself is your query UI, and if you already run Grafana for metrics (see Prometheus setup), you’re reusing it — one less thing to stand up.
Docker Compose setup
services:
loki:
image: grafana/loki:3.0.0
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml
- loki-data:/loki
command: -config.file=/etc/loki/local-config.yaml
promtail:
image: grafana/promtail:3.0.0
volumes:
- /var/log:/var/log:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./promtail-config.yaml:/etc/promtail/config.yaml
command: -config.file=/etc/promtail/config.yaml
volumes:
loki-data:
A minimal promtail-config.yaml that picks up Docker container logs:
server:
http_listen_port: 9080
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
target_label: 'container'
Point Grafana at Loki as a data source (http://loki:3100) and you can query logs from the Explore tab immediately.
Querying with LogQL
LogQL looks like PromQL because it’s built the same way — filter by label, then filter the text:
{container="nginx"} |= "error"
{container="api"} | json | status >= 500
The first line-filters raw text. The second parses JSON log lines and filters on a field — useful once your app logs structured JSON instead of plain strings.
Retention, so disk doesn’t fill up
Set a retention period in loki-config.yaml so old logs get deleted instead of accumulating forever:
limits_config:
retention_period: 168h # 7 days
compactor:
retention_enabled: true
delete_request_store: filesystem
Seven days is enough to debug most incidents without turning your log volume into a slow-motion disk-fill problem.
When this stops being enough
| You have | Use |
|---|---|
| 1-15 hosts, mostly container logs | Loki + Promtail |
| Need full-text search across log bodies, not just labels | Elasticsearch-based stack |
| Compliance requirement for long-term structured log retention | A managed log platform |
For most self-hosted setups, the failure mode isn’t “Loki can’t handle it” — it’s forgetting to set label cardinality sane (don’t put request IDs or timestamps in labels) which is the one way to blow Loki’s index up just like Elasticsearch’s.