Grafana Loki is a horizontally scalable log aggregation system inspired by Prometheus: it indexes only labels per log stream instead of full text, so it uses less storage and is easier to run—especially if you already use Prometheus or Kubernetes. Grafana Alloy collects logs (Docker container output and files under /var/log) and ships them to Loki; Grafana is the UI where you query logs with LogQL. This guide walks you through running Grafana Loki, Alloy, and Grafana with Docker and Docker Compose for local or dev use, then querying logs in Grafana Explore.
The stack below was tested with Loki 3.7.9, Alloy 1.21.3 and Grafana 13.3.0. The image tags are pinned; check the Loki, Alloy and Grafana release pages for newer versions.
In this guide you’ll:
- Verify Docker and Docker Compose, then run Grafana in a container
- Run Loki and Alloy with Docker using
loki-config.yamlandconfig.alloy - Run the full stack, with healthchecks and a provisioned Loki data source, with a single
docker compose up -d - Query container and system logs with LogQL in Explore
- Convert an existing Promtail config to Alloy
For production, use Loki in distributed mode with Helm or Tanka and persistent storage (e.g. S3, GCS).
Promtail is end of life as of March 2, 2026 and gets no further updates. Earlier versions of this guide used it; it now uses Grafana Alloy, its replacement. If you still run Promtail, see Migrating from Promtail below.
Related guides:
- How to run Grafana OSS in Docker and Docker Compose
- How to run Prometheus with Docker and Docker Compose
- How to set up Promtail, Grafana and Loki for log management in Debian 11
- How to Ship Logs to Loki With Grafana Alloy on Debian 12/13
- How to run Grafana Loki with Helm and Kustomize in Kubernetes
- How to install and configure Prometheus on a Linux server
Prerequisites: Docker and Docker Compose
You need Docker and Docker Compose installed. On Ubuntu, see How to Install and Use Docker Engine on Ubuntu 26.04 LTS.
Confirm that Docker is working as expected:
docker version
Docker Compose is now included with Docker Desktop and is available on Linux as a Docker plugin. Confirm it’s available with:
docker compose version
This guide uses the docker compose (v2) plugin. The old docker-compose v1 binary is no longer maintained.
Run Grafana in Docker
Grafana is an open source analytics and visualization platform. For this setup, Grafana is the UI for Loki: you’ll use it to add Loki as a data source, run LogQL queries in Explore, and build dashboards from your logs.
Create a Docker network that Grafana, Loki and Alloy will share, so they can reach each other by container name:
docker network create grafana_loki
Run Grafana OSS in a container with:
docker run -d --name grafana \
--network grafana_loki \
-p 3000:3000 \
grafana/grafana:13.3.0
The -d flag runs the container in the background. When it’s up, open Grafana at http://localhost:3000 (or http://your_server_ip:3000). The grafana/grafana image is the OSS edition; the older grafana/grafana-oss image name is no longer updated.
Run Loki with Docker
Loki is the log backend; Alloy is the agent that collects logs and sends them to Loki. Together they give you log aggregation and search that fits well with Prometheus and Kubernetes.
First, create a working directory:
mkdir -p loki && cd loki
Next, create a configuration file required to start and run Loki. Save this as loki-config.yaml:
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-04-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 744h
compactor:
working_directory: /loki/compactor
retention_enabled: true
delete_request_store: filesystem
analytics:
reporting_enabled: false
A few notes on this config:
store: tsdbwithschema: v13is the schema Loki 3.x expects. Older guides (including earlier versions of this one) useboltdb-shipperwithv11, which does not support newer features such as structured metadata.retention_period: 744h(31 days) plusretention_enabled: trueon the compactor makes Loki delete logs older than a month. Without it Loki keeps everything forever.analytics.reporting_enabled: falseturns off the anonymous usage report Loki sends to Grafana Labs by default.
You can check the file before you start Loki:
docker run --rm -v "$(pwd)/loki-config.yaml":/etc/loki/config.yaml:ro \
grafana/loki:3.7.9 -config.file=/etc/loki/config.yaml -verify-config
It should print msg="config is valid". Now run the Loki instance with this command:
docker run -d --name loki \
--network grafana_loki \
-v "$(pwd)/loki-config.yaml":/etc/loki/config.yaml:ro \
-v loki_data:/loki \
-p 3100:3100 \
grafana/loki:3.7.9 -config.file=/etc/loki/config.yaml
Once Loki is running, run Alloy to ship logs to it. This config collects the output of every container on the Docker host, plus the files matching /var/log/*log. Create a file named config.alloy:
// Find every container on this Docker host
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "5s"
}
// Turn Docker metadata into Loki labels
discovery.relabel "containers" {
targets = []
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
rule {
source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
target_label = "service"
}
}
// Ship container stdout/stderr
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
relabel_rules = discovery.relabel.containers.rules
labels = {job = "docker"}
forward_to = [loki.write.local.receiver]
}
// Ship plain-text log files from the host's /var/log
local.file_match "varlogs" {
path_targets = [{
__path__ = "/var/log/*log",
job = "varlogs",
}]
}
loki.source.file "varlogs" {
targets = local.file_match.varlogs.targets
forward_to = [loki.write.local.receiver]
}
// Where the logs go
loki.write "local" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
discovery.docker checks for new containers every 60 seconds by default; refresh_interval = "5s" picks them up faster, so short-lived containers are much less likely to be missed. The container and service labels come from the container name and its Compose service name.
Start Alloy with this command:
docker run -d --name alloy \
--network grafana_loki \
-v "$(pwd)/config.alloy":/etc/alloy/config.alloy:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /var/log:/var/log:ro \
-v alloy_data:/var/lib/alloy/data \
-p 12345:12345 \
grafana/alloy:v1.21.3 \
run /etc/alloy/config.alloy \
--storage.path=/var/lib/alloy/data \
--server.http.listen-addr=0.0.0.0:12345
When Loki and Alloy are running, check Loki’s metrics at http://localhost:3100/metrics and readiness at http://localhost:3100/ready (Loki takes about 15 seconds to report ready). Alloy’s UI at http://localhost:12345 shows each component and whether it is healthy.
Tip: Alloy needs the Docker socket to discover containers, and access to the socket is root-equivalent on the host. The :ro flag only stops the container replacing the socket file; it does not restrict the Docker API, so anything that can reach the socket can control the host. For a narrower grant, put a socket proxy such as tecnativa/docker-socket-proxy (with CONTAINERS=1) in front of it. Also do not expose port 12345 beyond your machine: the Alloy UI has no login.
Run the full stack with Docker Compose
A Docker Compose file lets you start and manage Grafana, Loki, and Alloy with one command. If you started the containers from the previous sections, remove them first so the names and ports are free:
docker rm -f grafana loki alloy
Use the same loki-config.yaml and config.alloy as above. Grafana can add the Loki data source for you at startup, so create a provisioning file:
mkdir -p grafana/provisioning/datasources
Save this as grafana/provisioning/datasources/loki.yaml:
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: false
Then add the following to a docker-compose.yaml file in the same directory:
networks:
grafana_loki:
volumes:
loki_data:
alloy_data:
grafana_data:
services:
loki:
image: grafana/loki:3.7.9
command: -config.file=/etc/loki/config.yaml
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/config.yaml:ro
- loki_data:/loki
networks:
- grafana_loki
restart: unless-stopped
alloy:
image: grafana/alloy:v1.21.3
command:
- run
- /etc/alloy/config.alloy
- --storage.path=/var/lib/alloy/data
- --server.http.listen-addr=0.0.0.0:12345
ports:
- "12345:12345"
volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/log:/var/log:ro
- alloy_data:/var/lib/alloy/data
healthcheck:
test: ["CMD", "bash", "-c", "exec 3<>/dev/tcp/127.0.0.1/12345 && printf 'GET /-/ready HTTP/1.0\\r\\n\\r\\n' >&3 && grep -q ' 200 ' <&3"]
interval: 10s
timeout: 5s
retries: 6
depends_on:
- loki
networks:
- grafana_loki
restart: unless-stopped
grafana:
image: grafana/grafana:13.3.0
ports:
- "3000:3000"
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning:ro
- grafana_data:/var/lib/grafana
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:3000/api/health"]
interval: 10s
timeout: 5s
retries: 6
depends_on:
- loki
networks:
- grafana_loki
restart: unless-stopped
The directory should now look like this:
loki/
├── config.alloy
├── docker-compose.yaml
├── grafana/provisioning/datasources/loki.yaml
└── loki-config.yaml
From that directory, start everything and wait for the healthchecks to pass:
docker compose up -d --wait
docker compose ps --format 'table {{.Service}}\t{{.Image}}\t{{.Status}}'
SERVICE IMAGE STATUS
alloy grafana/alloy:v1.21.3 Up 20 seconds (healthy)
grafana grafana/grafana:13.3.0 Up 20 seconds (healthy)
loki grafana/loki:3.7.9 Up 20 seconds
A few notes on the Compose file:
- The top-level
version:key is obsolete in Compose v2 and is left out. - The Loki image is distroless: it has no shell,
wgetorcurl, so it cannot run a Docker healthcheck. Check it from the host withcurl http://localhost:3100/ready. The Alloy image has nocurleither, so its healthcheck uses Bash’s/dev/tcpto call Alloy’s/-/readyendpoint. - Named volumes keep Loki’s chunks, Alloy’s read positions and Grafana’s database across restarts.
docker compose down -vdeletes them.
Grafana stays at port 3000, Loki at 3100, and the Alloy UI at 12345.
Check that logs reach Loki
Start a throwaway container that prints a line, then ask Loki’s API for it:
docker run -d --name log-test busybox:1.37 sh -c 'echo hello-loki; sleep 30'
sleep 15
curl -sG http://localhost:3100/loki/api/v1/query_range \
--data-urlencode 'query={container="log-test"} |= "hello-loki"' \
--data-urlencode 'limit=5'
docker rm -f log-test
The JSON response should contain a stream with "container":"log-test" and "job":"docker", and the value hello-loki.
Add Loki as a data source and query logs
- Log in to Grafana at
http://localhost:3000with the default useradminand passwordadmin. Change the password when prompted. - Add Loki: With the Docker Compose stack, this is already done: the provisioning file adds a Loki data source pointing at
http://loki:3100. If you started Grafana withdocker runinstead, open the menu (Grafana icon) → Connections → Data sources → Add data source → Loki. Set URL tohttp://loki:3100and click Save & test. - Query logs: Go to Explore, select Loki, then use the label browser to pick
job→docker(container logs) orjob→varlogs(for/var/log/*log). Run a query such as:
{job="docker", service="grafana"}
To see logs from one container that contain the word “error”:
{container="loki-loki-1"} |= "error"
Compose names containers <project>-<service>-<n>, where the project is the directory name, so adjust the container name to match docker compose ps. For system logs, parse logfmt lines and hide lines that fail parsing with:
{job="varlogs"} | logfmt | __error__=""
You should see logs from your containers and system streaming in.
Migrating from Promtail
If you already run Promtail in Docker, Alloy can convert its config for you. For example, with the promtail-config.yaml that earlier versions of this guide used, in the current directory:
docker run --rm -v "$(pwd)":/work -w /work grafana/alloy:v1.21.3 \
convert --source-format=promtail --output=/work/promtail.alloy /work/promtail-config.yaml
Writing to promtail.alloy keeps your existing config.alloy intact. The output for a config that tails /var/log/*log looks like this:
loki.source.file "system" {
targets = [{
__address__ = "localhost",
__path__ = "/var/log/*log",
job = "varlogs",
}]
forward_to = [loki.write.default.receiver]
file_match {
enabled = true
}
legacy_positions_file = "/positions/positions.yaml"
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
external_labels = {}
}
The converted config has only what Promtail did (here, the /var/log file source); it has no Docker discovery. Merge its blocks into the config.alloy above rather than replacing it, or the job="docker" queries return nothing. legacy_positions_file makes Alloy start from where Promtail stopped reading: add Promtail’s positions volume to the alloy service (for example - promtail_positions:/positions, and declare promtail_positions: under the top-level volumes:) to avoid shipping duplicate lines. Then replace the promtail service with the alloy service from the Compose file above, and run docker compose up -d --remove-orphans. See the official Promtail migration guide for the conversion’s limits, and How to Ship Logs to Loki With Grafana Alloy on Debian 12/13 for the same migration on a host without Docker.
Frequently Asked Questions (FAQ)
What is Grafana Loki?
Grafana Loki is a log aggregation system that indexes only label metadata for log streams (similar to how Prometheus indexes metrics), not full log content. That reduces storage and operational cost. You query logs with LogQL in Grafana after adding Loki as a data source.
What is Grafana Alloy, and what happened to Promtail?
Grafana Alloy is Grafana Labs’ collector for logs, metrics and traces. For Loki it does what Promtail used to do: it discovers log sources (Docker containers, files under /var/log, the systemd journal), adds labels, and pushes log lines to Loki. Promtail reached end of life on March 2, 2026 and gets no further updates, so new setups should use Alloy. alloy convert --source-format=promtail converts an existing Promtail config.
How do I run Loki with Docker Compose?
Create a docker-compose.yaml with services for loki, alloy, and grafana on a shared network. Mount loki-config.yaml and config.alloy, mount /var/run/docker.sock into Alloy, use named volumes for Loki, Alloy and Grafana data, and provision the Loki data source in Grafana. Then run docker compose up -d --wait. Loki listens on port 3100, Grafana on 3000.
Can I use this Loki Docker setup in production?
This guide uses single-binary Loki with filesystem storage and in-memory ring, which is suitable for local or dev. For production, run Loki in distributed mode with persistent object storage (S3, GCS) and deploy via Helm or Tanka.
What is LogQL?
LogQL is Loki’s query language (inspired by PromQL). In Grafana Explore with Loki selected, you use it to filter by labels (e.g. {job="docker"}) and optionally parse or filter log lines (e.g. |= "error" or | logfmt).
Conclusion
You ran Grafana, Loki, and Grafana Alloy with Docker and Docker Compose, provisioned Loki as a Grafana data source, and queried container and system logs in Grafana Explore using LogQL. For next steps, add dashboards and alerts, or pair this stack with Prometheus in Docker for metrics and logs together.