Grafana Loki Tutorial 2026: Centralize Linux and Docker Logs Without Elasticsearch

Grafana Loki Tutorial 2026: Centralize Linux and Docker Logs Without Elasticsearch

Centralizing logs used to mean standing up an Elasticsearch cluster, tuning JVM heap sizes, and paying for index storage that grows without mercy. Grafana Loki changes that trade-off entirely. This grafana loki tutorial walks you through deploying the full PLG stack — Promtail, Loki, and Grafana — on a single host using Docker Compose, shipping both Linux syslog and Docker container logs, querying everything with LogQL, and wiring up alerts — all without touching Elasticsearch.


1. Why Loki Instead of Elasticsearch?

The ELK stack (Elasticsearch, Logstash, Kibana) indexes every field in every log line at ingest time. That full-text index gives powerful search but consumes enormous disk and memory — often 10–30× the raw log volume, plus a JVM process that wants at least 16 GB of heap on day one.

Loki takes the opposite approach: it stores compressed log chunks and indexes only labels (key=value pairs attached at scrape time). Querying scans the relevant chunks, much like how Prometheus queries time-series data. The result is dramatically lower storage cost and a far simpler operational footprint. This loki log aggregation tutorial is built on that foundation.

CriterionElasticsearch (ELK)Grafana Loki
Index strategyFull-text on every fieldLabels only
Storage costHigh (10–30× raw)Low (~1–2× raw)
Minimum RAM (single node)16 GB (JVM)512 MB – 2 GB
Query languageLucene / KQLLogQL (PromQL-inspired)
Schema required at ingestYesNo
Horizontal scalingComplex (shards, replicas)Simple (object storage)
Grafana integrationThird-party pluginNative datasource
Best fitFull-text search, rich aggregationsHigh-volume ops and container logs

For teams already running Prometheus and Grafana, Loki is the natural ELK alternative loki path: one unified observability UI, one alert engine, no JVM, no mapping migrations.


2. The PLG Stack Overview

The PLG stack is three components working in sequence:

  • Promtail — the log shipper. It tails local files or reads the Docker socket and forwards labeled log streams to Loki over HTTP.
  • Loki — the storage and query engine. It receives log streams, chunks and compresses them, and answers LogQL queries.
  • Grafana — the visualization layer. It queries Loki via its native datasource and renders log panels alongside Prometheus metric graphs.

Data flow: log file / Docker socket → Promtail → Loki → Grafana.

The key insight is that Promtail and Loki share the same label vocabulary as Prometheus. A label like {app="nginx", env="production"} means the same thing whether it tags a metric or a log stream. This consistency is what makes the PLG stack feel unified rather than bolted together.


3. Deploy with Docker Compose

Create a project directory with the following layout:

plg-stack/
  docker-compose.yml
  loki-config.yaml
  promtail-config.yaml

docker-compose.yml:

version: "3.8"

networks:
  loki:

volumes:
  loki-data:
  grafana-data:

services:
  loki:
    image: grafana/loki:2.9.4
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
      - loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki
    restart: unless-stopped

  promtail:
    image: grafana/promtail:2.9.4
    container_name: promtail
    volumes:
      - ./promtail-config.yaml:/etc/promtail/config.yaml
      - /var/log:/var/log:ro
      - /var/run/docker.sock:/var/run/docker.sock
    command: -config.file=/etc/promtail/config.yaml
    networks:
      - loki
    depends_on:
      - loki
    restart: unless-stopped

  grafana:
    image: grafana/grafana:10.4.2
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - loki
    depends_on:
      - loki
    restart: unless-stopped

Start the stack:

docker compose up -d
docker compose ps

All three containers should be running within about 30 seconds. Grafana is available at http://localhost:3000 (login: admin / changeme).


4. Loki Configuration

Create loki-config.yaml in the project directory:

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

common:
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    instance_addr: 127.0.0.1
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 744h        # 31 days

compactor:
  working_directory: /loki/compactor
  retention_enabled: true

analytics:
  reporting_enabled: false

Key settings explained:

  • auth_enabled: false — suitable for a local or single-tenant deployment. Set to true and require the X-Scope-OrgID header before exposing Loki to a network.
  • filesystem storage — sufficient for a single node. For production scale or multi-node deployments, replace with an S3-compatible object store (Amazon S3, MinIO, GCS).
  • retention_period: 744h — the compactor deletes chunks older than 31 days. Adjust based on your storage budget.
  • schema: v13 with store: tsdb — the current recommended index format since Loki 2.8.

5. Ship Linux Syslog with Promtail

Create promtail-config.yaml with a scrape config targeting /var/log/syslog:

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: syslog
    static_configs:
      - targets:
          - localhost
        labels:
          job: syslog
          host: my-linux-host
          __path__: /var/log/syslog

Label design matters: Loki indexes only the labels you attach at scrape time.

  • job: syslog — identifies the log source type. Use this in every query to narrow which chunks Loki fetches.
  • host: my-linux-host — the hostname. Keeps streams from different servers separate without high-cardinality blowup.

Promtail records its file read position in positions.yaml. On restart it resumes from where it left off, so no log lines are re-shipped. The /var/log directory is mounted read-only into the Promtail container via the volume in docker-compose.yml.


6. Ship Docker Container Logs with Promtail

Append a second entry to scrape_configs in promtail-config.yaml to enable Docker socket discovery:

  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 10s
    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: service
      - target_label: job
        replacement: docker

Promtail connects to the Docker socket (/var/run/docker.sock, mounted in the Compose file), discovers every running container, and tails its log stream automatically. When you start a new container, Promtail discovers it within refresh_interval seconds — no reload needed.

The relabel_configs block extracts useful metadata and promotes it to Loki stream labels:

  • container — the container name (e.g., nginx, api-server).
  • streamstdout or stderr.
  • service — the Docker Compose service name, if applicable.
  • job: docker — lets you select all Docker logs with {job="docker"}.

Avoid promoting high-cardinality fields (request IDs, user IDs) as labels. They create millions of separate streams, which degrades ingester memory and query performance.


7. LogQL Queries

This logql tutorial covers the three query patterns you will reach for most often.

Filter by label and string

{job="docker"} |= "error"

{job="docker"} is the stream selector — it narrows which chunks Loki fetches. |= "error" is a line filter — it keeps only lines containing the literal string error. LogQL line filter operators:

OperatorMeaning
\|=Line contains string
!=Line does not contain string
\|~Line matches regex
!~Line does not match regex

Compute log ingestion rate

rate({job="syslog"}[5m])

rate() returns the per-second rate of log lines over the last 5 minutes, exactly like rate() in PromQL. Use this in a Grafana time-series panel to visualize log volume trends and detect sudden spikes that precede an incident.

Parse JSON logs and filter by extracted field

{job="app"} | json | status >= 500

The | json stage parses each log line as JSON and promotes its keys to extracted fields. | status >= 500 then filters on the numeric value of the extracted status field. Use | logfmt instead if your application emits key=value formatted lines.

Multiple pipeline stages chain with |. Place the most selective label filter first so Loki fetches the smallest possible set of chunks before applying the more expensive line-level parsing.


8. Build a Grafana Dashboard

Connect Grafana to Loki:

  1. Open Grafana at http://localhost:3000.
  2. Go to Connections → Data sources → Add data source.
  3. Choose Loki, set the URL to http://loki:3100, and click Save & test. You should see "Data source connected and labels found."

Create a dashboard with two panels side by side:

Panel 1 — Log viewer (Logs visualization)

  • Visualization: Logs
  • Query: {job="docker"} |= "error"
  • Options: enable Deduplication, Wrap lines, Show time

This panel streams matching log lines in real time. Lines are colored by severity if your logs contain a level or severity label.

Panel 2 — Log rate per container (Time series visualization)

  • Visualization: Time series
  • Query: sum(rate({job="docker"}[5m])) by (container)
  • Legend: {{container}}

This panel shows the ingestion rate per container over time. A spike in one container while others stay flat is a reliable early indicator of a noisy or failing service.

Save the dashboard. Use the time range picker in the top-right corner to zoom into a specific incident window. Dashboard variables (label_values(container)) let users filter dynamically to a specific container without editing the query.


9. Loki Alerts with Grafana

Grafana's unified alerting engine evaluates LogQL metric queries on a schedule and fires alerts to any configured contact point (Slack, PagerDuty, email).

Create an alert rule for sustained error rate:

  1. Navigate to Alerting → Alert rules → New alert rule.
  2. Select the Loki data source and enter a metric query:
sum(rate({job="docker"} |= "error" [5m])) by (container)
  1. Set the condition: IS ABOVE 0.5 (0.5 error lines per second).
  2. Set evaluation interval to 1m, pending period to 5m — the alert fires only after the condition holds for five consecutive minutes, reducing noise from short-lived spikes.
  3. Add labels: severity=warning, team=ops.
  4. Attach a contact point in Alerting → Contact points.

For alerts that need to fire independently of the Grafana server (for example in high-availability setups), configure the Loki ruler component with YAML alerting rules stored in the Loki rules directory. The rule format mirrors Prometheus alerting rules, with a LogQL expression in the expr field.


10. Loki vs Elasticsearch: Detailed Comparison

CriterionElasticsearch (ELK)Grafana Loki
Indexing modelInverted index on all fieldsLabel index only
Storage efficiency~1.5 GB per GB of raw logs~0.1–0.15 GB per GB of raw logs
Ingest cost at 50 GB/day (cloud)$300–$600/mo$15–$50/mo
Minimum RAM (single node)16 GB (JVM heap)512 MB – 2 GB
Full-text search speedFast (index lookup)Slower (chunk scan) without label filter
Ops complexityHigh (mappings, ILM, shard management)Low (config file + compactor)
Multi-tenancyIndex-per-tenant or aliasesNative X-Scope-OrgID header
Native Grafana integrationThird-party datasourceBuilt-in, same model as Prometheus

Choose Loki when your team runs Prometheus and Grafana, log volume is large and cost-sensitive, and your queries are filtered by application and environment labels.

Choose Elasticsearch when you need full-text token search without any label pre-filter, rich aggregations across arbitrary fields, or mature commercial compliance and security features.

A common 2026 pattern is running both: Loki for high-volume application and container logs, Elasticsearch for lower-volume security event logs and audit trails that require full-text search.


11. FAQ

Q: Can Loki replace Elasticsearch entirely? For application logs, container logs, and syslog — yes, in most cases. If you need to search for an arbitrary string across all logs without any label selector, Loki must scan every chunk, which is slow. Elasticsearch's full invert-index makes that query instant. For compliance and audit log scenarios with strict full-text search requirements, Elasticsearch remains the stronger choice.

Q: How do I scale Loki beyond a single node? Switch to object storage (S3, GCS, or MinIO) and run Loki in microservices mode with separate distributor, ingester, querier, and query-frontend pods. The Grafana-maintained Helm chart handles the deployment topology. Read and write paths scale independently.

Q: Does Promtail support Windows? Promtail has a Windows Event Log scrape target. For a smoother Windows experience, the Grafana Agent (which embeds Promtail) is the recommended alternative.

Q: What is the maximum safe label cardinality? Keep the number of unique label value combinations (stream count) below a few thousand per Loki instance. High-cardinality identifiers like user_id, request_id, or trace_id must not be used as index labels. Extract them at query time with | json or | regexp instead.

Q: Can I use Loki without Promtail? Yes. Loki's HTTP push API (/loki/api/v1/push) is compatible with Fluent Bit (native output plugin), Fluentd, the OpenTelemetry Collector, Vector, Logstash, and the Docker loki logging driver. Many Kubernetes teams prefer Fluent Bit for its lower resource footprint compared to Promtail.

Q: Is Loki production-ready in 2026? Yes. Grafana Labs runs Loki at petabyte scale internally. The TSDB v13 schema (used in this tutorial) is the stable, recommended index format. The main operational risk is cardinality explosion from poorly chosen labels — audit stream counts regularly in Grafana's Explore view with {job=~".+"} and the stream count metric loki_ingester_streams_created_total.


Grafana Loki delivers centralized log aggregation at a fraction of the cost and complexity of the ELK stack. With the setup above you have a complete promtail tutorial pipeline in production: Promtail ships Linux syslog and Docker container logs, Loki stores and label-indexes them efficiently, and Grafana surfaces them in real-time dashboards and alerts — all from a single docker compose up -d, without an Elasticsearch cluster in sight.

Leonardo Lazzaro

Software engineer and technical writer. 10+ years experience in DevOps, Python, and Linux systems.

More articles by Leonardo Lazzaro