IAnswerable

Aggregate Logs Without an ELK Stack

02 Oct 2026

An 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:

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.