Centralized Logging with Grafana Loki
Every container and host's logs in one searchable place, viewed right alongside your metrics dashboards -- no more SSHing into five machines to find one error.
What you'll build and why
Loki stores and indexes logs the way Prometheus stores and indexes metrics -- but instead of indexing every word in every log line (expensive, and what makes tools like Elasticsearch heavy to run), it indexes only a small set of labels (which host, which container, which service) and keeps the log text itself compressed and cheap. Grafana Alloy ships logs into it. The payoff is viewing logs in the same Grafana instance as your metrics, and being able to jump from "this graph spiked at 2:14pm" straight to the logs from that exact window.
Don't build this if: you're only running a handful of services and docker logs <container> genuinely covers your needs -- this is worth the setup once you're regularly hunting across more than a couple of hosts or containers for one answer.
How it works
Docker containers (stdout/stderr logs)
│
▼
Grafana Alloy (on each host) -- collects and ships
│ logs to Loki
▼
Loki -- indexes by label (host, container, service),
│ stores compressed log content
│
▼
Grafana -- same instance as your metrics dashboards,
queried with LogQL
The label-only indexing is the whole design: Loki doesn't know what's inside a log line until you query it, which keeps ingestion cheap but means broad, unfiltered text searches across huge volumes are slower than a purpose-built search engine. For homelab-scale logs, this tradeoff is the right one.
Before you start
Decision: Alloy, not Promtail. Promtail was Loki's original log-shipping agent and is still documented in older guides, but Grafana's current guidance is to use Grafana Alloy for new setups -- it's the actively developed collector going forward. This guide uses Alloy throughout.
Steps
Step 1: Run Loki
services:
loki:
image: grafana/loki:latest
container_name: loki
ports:
- "3100:3100"
volumes:
- "loki_data:/loki"
command: -config.file=/etc/loki/local-config.yaml
restart: unless-stopped
volumes:
loki_data:
The default config bundled in the image is a reasonable single-node starting point -- no separate config file needed for an initial setup.
Step 2: Run Grafana Alloy to collect Docker logs
services:
alloy:
image: grafana/alloy:latest
container_name: alloy
volumes:
- "./alloy-config.alloy:/etc/alloy/config.alloy"
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "/var/lib/docker/containers:/var/lib/docker/containers:ro"
command: run --server.http.listen-addr=0.0.0.0:12345 /etc/alloy/config.alloy
ports:
- "12345:12345"
restart: unless-stopped
alloy-config.alloy (Alloy's configuration language, minimal Docker-log-shipping setup):
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
}
loki.source.docker "default" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://<loki-host>:3100/loki/api/v1/push"
}
}
This discovers running containers on the Docker socket and ships their logs to Loki, labeled by container name automatically.
Step 3: Add Loki as a data source in Grafana
Reuse the Grafana instance from the Prometheus guide (prerequisite): Connections → Data sources → Add → Loki, URL http://<loki-host>:3100.
Step 4: Query logs with LogQL
In Grafana's Explore view, select the Loki data source and query by label:
{container="jellyfin"}
Narrow to matching text within that label selection:
{container="jellyfin"} |= "error"
Verify it works
- Grafana's Explore view with the Loki data source selected shows live logs from at least one container
-
{container="<name>"}returns logs scoped to just that container, confirming labels are being applied correctly - A text-filtered query (
|= "error"or similar) actually narrows results rather than returning everything - Failure test: stop Alloy (
docker stop alloy) and generate some log output from a container in the meantime (restart it, or trigger an action). Confirm that gap is genuinely missing from Loki once Alloy comes back -- Alloy ships logs going forward, it does not retroactively backfill what it missed while down, which is a real limitation worth knowing before you rely on this for an incident review
Secure it
- What's exposed: Loki's push API and Grafana should stay LAN-only -- Loki has no built-in authentication in a default single-node setup, so anything that can reach port 3100 can write logs to it or, more importantly, read every log Loki has stored.
- What's in your logs: application logs can contain sensitive data (tokens, IPs, occasionally credentials logged in error) without you having deliberately put it there -- be aware of what your services actually log before treating Loki as a low-sensitivity system.
- Docker socket access: Alloy's read-only mount of the Docker socket is still meaningful access -- understand this is similar in kind to the Docker-socket exposure discussed in the Docker Compose homelab guide, even though it's read-only here.
Back it up and maintain it
What matters: Alloy's config file (small, worth version-controlling). Loki's actual log data is generally not worth backing up for a homelab -- it's operational history, not irreplaceable state, similar to Prometheus's metric data.
Update cadence: Loki and Alloy both ship regularly; update on your own schedule, watching for config-syntax changes between versions (Alloy's config language is newer and has changed shape before).
What to monitor: Alloy's own logs (docker logs alloy) for shipping failures -- a silently-stopped shipper means you have a growing blind spot without any obvious symptom.
Troubleshooting
Logs: docker logs loki, docker logs alloy.
| Symptom | Likely cause | Diagnostic | Fix |
|---|---|---|---|
| No logs appear in Grafana at all | Alloy isn't running, or its Loki endpoint URL is wrong | docker logs alloy for shipping errors; confirm the url in loki.write matches Loki's actual address |
Fix the endpoint URL or restart Alloy |
| Some containers' logs are missing | Alloy's Docker discovery started after that container, or the container was already stopped when Alloy started | Check docker ps against what's showing up in Grafana's label list |
Restart the missing container, or confirm Alloy's discovery is picking up new containers as they start |
| LogQL query returns nothing for a label you expect to exist | Label value doesn't match what you typed (case-sensitive, or Alloy auto-generated a slightly different name) | In Grafana's Explore view, browse the label list directly rather than guessing the value | Use the actual label value shown in the browser |
| Loki's disk usage growing faster than expected | Retention period longer than needed, or unexpectedly high log volume from a noisy container | Check Loki's configured retention; check which container's logs dominate via a per-container query | Adjust retention; consider reducing log verbosity on the noisy service if it's not actually useful |
| Alloy container repeatedly restarts | Config file syntax error | docker logs alloy shows the parse error directly |
Fix the .alloy config syntax per Alloy's own documentation |
Undo
docker compose down removes both containers; delete the loki_data volume to remove stored logs. Remove the Loki data source from Grafana.
Go further
- Ship logs from non-Docker sources too (a bare-metal host's syslog, a Proxmox host) using Alloy's other collection methods, not just the Docker log source used here
- Set up a Grafana alert on log content (e.g. a spike in
"error"-matching lines) as a complement to the metric-based alerts in the Prometheus guide - Explore Loki's structured metadata feature for higher-cardinality fields without the indexing cost of full labels
Resources
Official documentation:
- Grafana Loki documentation -- architecture and query language, verified this session
- Grafana Alloy documentation -- current recommended collector, verified this session
Source and releases:
Community:
Related DaemonPress projects:
- Metrics and Alerting with Prometheus, Grafana, and Alertmanager -- the metrics half of this same Grafana instance
- Managing a Homelab with Docker Compose, Portainer, and Watchtower
Last verified: 2026-09-21, checked against official Grafana Loki and Alloy documentation.