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.
| Criterion | Elasticsearch (ELK) | Grafana Loki |
|---|---|---|
| Index strategy | Full-text on every field | Labels only |
| Storage cost | High (10–30× raw) | Low (~1–2× raw) |
| Minimum RAM (single node) | 16 GB (JVM) | 512 MB – 2 GB |
| Query language | Lucene / KQL | LogQL (PromQL-inspired) |
| Schema required at ingest | Yes | No |
| Horizontal scaling | Complex (shards, replicas) | Simple (object storage) |
| Grafana integration | Third-party plugin | Native datasource |
| Best fit | Full-text search, rich aggregations | High-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 totrueand require theX-Scope-OrgIDheader before exposing Loki to a network.filesystemstorage — 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: v13withstore: 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).stream—stdoutorstderr.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:
| Operator | Meaning |
|---|---|
\|= | 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:
- Open Grafana at
http://localhost:3000. - Go to Connections → Data sources → Add data source.
- 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:
- Navigate to Alerting → Alert rules → New alert rule.
- Select the Loki data source and enter a metric query:
sum(rate({job="docker"} |= "error" [5m])) by (container)
- Set the condition: IS ABOVE
0.5(0.5 error lines per second). - Set evaluation interval to
1m, pending period to5m— the alert fires only after the condition holds for five consecutive minutes, reducing noise from short-lived spikes. - Add labels:
severity=warning,team=ops. - 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
| Criterion | Elasticsearch (ELK) | Grafana Loki |
|---|---|---|
| Indexing model | Inverted index on all fields | Label 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 speed | Fast (index lookup) | Slower (chunk scan) without label filter |
| Ops complexity | High (mappings, ILM, shard management) | Low (config file + compactor) |
| Multi-tenancy | Index-per-tenant or aliases | Native X-Scope-OrgID header |
| Native Grafana integration | Third-party datasource | Built-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.