How to Centralize Logs with Grafana Loki in Docker
Table of contents
- Key takeaways
- What is Loki and how does it differ from Elasticsearch?
- What does the docker-compose.yml for Loki look like?
- How do you ship logs with the Docker driver or Grafana Alloy?
- How do you query logs with LogQL in Grafana?
- How do you configure retention and storage?
- Frequently asked questions
- Does Loki replace Elasticsearch for logs?
- Why should I not use Promtail on a new install?
- How much does Loki consume on a home server?
- Conclusion
- Sources
Grafana Loki is an open-source log aggregation system that indexes only labels and keeps the content compressed, which makes storage ten to a hundred times cheaper than Elasticsearch. With Docker you bring it up with a single docker-compose.yml, ship your logs with the Docker driver or Grafana Alloy and query them with LogQL in Grafana on port 3100.
When you have ten or fifteen containers spread across the server, following their logs with docker logs one by one stops being viable: you need to centralize them in a single place to search. Grafana Loki is the piece built for exactly that, and with Docker you bring it up with a single docker-compose.yml. In this guide you will see what Loki is and why it indexes differently from Elasticsearch, how to start it alongside Grafana, how to ship logs to it with the Docker driver or Grafana Alloy, how to query them with LogQL and how to control retention. The same explanation is available in Spanish.
Key takeaways
- Grafana Loki is an open-source log aggregation system built by Grafana Labs; the most recent stable version is Loki 3.7.3, released on 24 June 2026, in the 3.7 branch.
- Unlike Elasticsearch, Loki only indexes the labels (metadata) of each log stream and stores the text compressed in chunks, which makes storage 10 to 100 times cheaper for the same volume.
- The HTTP API and the status UI listen on port 3100, and logs are sent over HTTP to the
/loki/api/v1/pushendpoint. - Promtail reached end-of-life on 2 March 2026: for new clients, Grafana recommends Grafana Alloy or the official Docker logging driver, not Promtail.
- By default Loki keeps logs forever (
retention_period: 0s); actual retention is enforced by the Compactor component, with a configurable minimum of 24 hours. - Beware of the
:latesttag: always pin a specific version such asgrafana/loki:3.7.3so you know exactly what you deploy and when to upgrade.
What is Loki and how does it differ from Elasticsearch?
Loki is an open-source log aggregation system that Grafana Labs describes as "Prometheus, but for logs". The comparison is not accidental: it shares Prometheus’s label model and its index-little philosophy. Its job is to receive the log lines from all your services, store them and let you search them afterwards from Grafana with its own language, LogQL.
The decisive difference from the ELK stack (Elasticsearch, Logstash, Kibana) is in what gets indexed. Elasticsearch builds an inverted index of every word of every line: that makes full-text search very fast, but the index usually weighs as much as the data itself and memory and disk go through the roof. Loki takes the opposite path: it indexes only a handful of labels per stream (for example job, container_name, level) and stores the content as it is, compressed in chunks on object storage or disk. The result is that storage comes out 10 to 100 times cheaper for the same log volume.
That decision has a trade-off worth understanding from the start: labels must be low cardinality. If you use as a label something that changes constantly, like the client IP or a request ID, you generate thousands of distinct streams and Loki becomes slow. The rule is simple: label by origin (service, container, environment) and leave the variable values inside the log text, where LogQL filters them at query time.
What does the docker-compose.yml for Loki look like?
You need two files in the same folder: the Loki configuration (loki-config.yaml) and the docker-compose.yml that starts Loki alongside Grafana. Start with a minimal configuration in monolithic mode (the whole binary in one container), with on-disk storage and retention enabled:
auth_enabled: false
server:
http_listen_port: 3100
common:
instance_addr: 127.0.0.1
path_prefix: /loki
replication_factor: 1
ring:
kvstore:
store: inmemory
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
schema_config:
configs:
- from: "2024-01-01"
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
retention_delete_delay: 2h
limits_config:
retention_period: 744h
reject_old_samples: true
reject_old_samples_max_age: 168h
And now the docker-compose.yml, which pins the versions, mounts that configuration read-only, persists the data in named volumes and publishes the ports only locally:
services:
loki:
image: grafana/loki:3.7.3
restart: unless-stopped
command: -config.file=/etc/loki/loki-config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml:ro
- loki_data:/loki
ports:
- "127.0.0.1:3100:3100"
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:3100/ready"]
interval: 15s
timeout: 5s
retries: 5
grafana:
image: grafana/grafana:12.3.0
restart: unless-stopped
depends_on:
- loki
environment:
- GF_AUTH_ANONYMOUS_ENABLED=false
volumes:
- grafana_data:/var/lib/grafana
ports:
- "127.0.0.1:3000:3000"
volumes:
loki_data:
grafana_data:
Note a couple of details. The loki_data volume is mounted at /loki, the path under which Loki writes the chunks, the index and the Compactor data; if you want to understand the difference between a named volume and a bind mount, we explain it in the guide on Docker volumes and bind mounts. Publishing the ports as 127.0.0.1:3100:3100 and 127.0.0.1:3000:3000 keeps both Loki and Grafana reachable only from the machine itself, because Loki ships with no authentication (auth_enabled: false): exposing it on the Internet without a proxy in front is a risk. When you want to publish Grafana with HTTPS and a domain, put a reverse proxy in front as explained in how to install Traefik with Docker Compose.
With both files ready, start the stack and check the status:
docker compose up -d
docker compose ps
curl -s http://127.0.0.1:3100/ready
When curl returns ready, Loki is ready to receive logs. Then open http://127.0.0.1:3000, log into Grafana and add a data source of type Loki with the URL http://loki:3100, using the service name inside the Compose network. The step by step for that part is in how to install Grafana with Docker.
How do you ship logs with the Docker driver or Grafana Alloy?
Loki is now running, but it still receives nothing: what is missing is the agent that collects the lines and pushes them to the /loki/api/v1/push endpoint. You have two common paths.
The most direct is the official Docker logging driver, a plugin that redirects each container’s standard output to Loki without any extra agent. It is installed once:
docker plugin install grafana/loki-docker-driver:3.7.0-amd64 \
--alias loki --grant-all-permissions
On ARM64 hosts use the -arm64 suffix. From there, any service can send its logs to Loki by adding a logging block to its Compose definition:
services:
my-app:
image: nginx:1.29
logging:
driver: loki
options:
loki-url: "http://localhost:3100/loki/api/v1/push"
loki-external-labels: "job=docker,container={{.Name}}"
Each line arrives at Loki tagged with job=docker and the container name, which is exactly the low cardinality Loki appreciates. Keep in mind a warning from the documentation itself: the driver can deadlock if Loki becomes unreachable, so in production many prefer the second path.
That second path is Grafana Alloy, the collector that replaces Promtail (retired on 2 March 2026). Alloy runs as one more container, reads logs from Docker or from files, adds labels to them and sends them to Loki, and does not carry the driver’s deadlock problem. For a homelab, start with the Docker driver for its simplicity and move to Alloy when you want to route, filter or transform the logs before storing them. If you also want a quick live view of the logs without writing queries, a lightweight viewer such as Dozzle complements Loki well.
How do you query logs with LogQL in Grafana?
LogQL is Loki’s query language, and anyone who knows PromQL will recognize it at once. Every query has two parts: a stream selector, with labels in braces, and an optional line filter. The simplest query selects all the logs from one origin:
{job="docker"}
{container="my-app"} |= "error"
{job="docker"} | json | level="error"
The first brings all logs with the job=docker label. The second adds a line filter |= "error", which keeps only the lines that contain that string (use != to exclude, |~ for regular expressions). The third goes one step further: | json parses each line as JSON and extracts its fields, so you can filter by level="error" even though level is not an indexed label. That is Loki’s division of labour: few labels to locate the stream fast, and filters over the text for the detail.
In Grafana, write these queries in the Explore panel with Loki as the data source and you will see the matching lines instantly, with a frequency histogram on top. LogQL can also generate metrics from logs; for example, rate({job="docker"} |= "error" [5m]) computes how many errors per second appear over the last five minutes, a number you can chart in a panel or use in an alert.
How do you configure retention and storage?
This is the point that most surprises people coming from other tools: by default, Loki deletes nothing. If you do not configure retention, the retention_period is 0s and logs live forever, so the disk grows unchecked. Retention is enforced by the Compactor, the background process that merges the many small indexes into a few larger ones and, along the way, removes the expired chunks.
In this guide’s loki-config.yaml it is already enabled with three pieces: retention_enabled: true inside the compactor block (without this, the Compactor only compacts, it does not delete), delete_request_store: filesystem so it knows where to record the deletion requests, and retention_period: 744h in limits_config, which sets a global retention of 31 days. The minimum Loki accepts is 24 hours, and the index period must also be 24 hours for retention to work.
You can fine-tune further and give different retentions per stream through retention_stream rules, for example keeping noisy access logs for 7 days and audit logs for 90 days. If disk is your limit, the other lever is to control how much each container takes at the source with Docker’s log-rotation options, something we cover in resource limits and logging in Docker. And when the volume really grows, Loki can store the chunks in S3 object storage instead of local disk, without changing anything else in the flow.
Frequently asked questions
Does Loki replace Elasticsearch for logs?
For most self-hosted cases, yes, and at far lower cost. Loki covers what you really need day to day: centralizing the logs of all your containers, searching them by service and filtering by text or by JSON fields. You give up Elasticsearch’s indexed full-text search, which only pays off if you do heavy analytics over years of logs. For a homelab or a server with dozens of services, Loki spends a fraction of the disk and memory and integrates natively with Grafana.
Why should I not use Promtail on a new install?
Because Promtail reached end-of-life on 2 March 2026. For years it was the recommended agent to send logs to Loki, but Grafana has replaced it with Grafana Alloy, which does the same and also collects metrics and traces with the same configuration. On a new install, use the Docker logging driver for the simple case and Alloy for everything else; keep Promtail only if you maintain a legacy deployment you cannot migrate yet.
How much does Loki consume on a home server?
Little, as long as you respect the low cardinality of the labels. In monolithic mode, with a handful of containers sending logs, Loki runs comfortably in a few hundred megabytes of RAM, well below what an equivalent Elasticsearch node asks for. Disk depends on retention and volume: with the chunk compression and a 31-day retention, a typical homelab stays within a few gigabytes.
Conclusion
With a docker-compose.yml, a loki-config.yaml and the Docker logging driver you have all your server’s logs centralized, searchable with LogQL from Grafana and with a retention controlled by the Compactor. The key is Loki’s model: index only low-cardinality labels, store the text compressed and filter at query time, which is what makes it so cheap compared to Elasticsearch. From here, connect Grafana dashboards, define alerts on the error rate and, when the volume calls for it, move the storage to S3 without touching the rest of the stack.