If you manage more than a handful of Docker containers, you already know the pain. SSH into a node, scroll through docker logs -f, grep for error strings, repeat. No central search. No retention policy. No correlation with metrics.
Grafana Loki solves this. It is a log aggregation system designed around the same philosophy as Prometheus — cheap, scalable, and tightly integrated with Grafana. Unlike Elasticsearch, Loki indexes only metadata labels and compresses the log content itself, so it runs comfortably on a homelab server with 2 GB of RAM.
This post walks through a complete Loki stack deployment with Grafana Alloy (the modern replacement for Promtail) as the log collector, all in Docker Compose.
Architecture: How the Stack Fits Together
The stack has three components:
- Grafana Alloy — watches Docker containers, scrapes stdout/stderr, attaches metadata labels, ships logs to Loki
- Loki — stores and indexes log streams, exposes LogQL query API on port 3100
- Grafana — queries Loki, builds dashboards, fires alerts
Alloy replaces Promtail. It supports the same pipelines but adds native OpenTelemetry, Prometheus scraping, and a more flexible configuration language.
Deploying Loki with Docker Compose
Create a project directory:
|
|
Start with docker-compose.yml. Loki runs as a single binary and in single-process mode for homelab use — no microservices needed:
|
|
The config file loki-config.yml:
|
|
Key settings:
schema: v13withstore: tsdbis the current recommended schema — fast queries, low memoryreject_old_samples_max_age: 168hdiscards logs older than 7 days at ingestioningestion_rate_mb: 10is fine for homelab; bump to 50 if you run 50+ containers
Deploying Grafana Alloy for Log Collection
Grafana Alloy watches the Docker socket and streams container logs to Loki. Add it to docker-compose.yml:
|
|
The Alloy config file alloy-config.alloy uses the River configuration language:
// Discover all running Docker containers
discovery.docker "all_containers" {
host = "unix:///var/run/docker.sock"
}
// Scrape logs from discovered containers
loki.source.docker "container_logs" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.all_containers.targets
forward_to = [loki.process.add_compose_label]
}
// Add useful labels from Docker metadata
loki.process "add_compose_label" {
forward_to = [loki.write.loki_endpoint]
stage.static_labels {
values = {
source = "docker",
}
}
stage.labels {
values = {
container_name = "",
compose_project = "",
container_image = "",
}
}
}
// Ship to Loki
loki.write "loki_endpoint" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}
What this does:
discovery.dockerdiscovers running containers via the Docker socketloki.source.dockertails stdout/stderr from each containerloki.processattaches labels — container_name, compose_project, container_imageloki.writepushes everything to Loki
No sidecar containers, no logging driver changes in other Compose stacks. Alloy handles it transparently.
Deploying Grafana with Loki Datasource
Add Grafana to docker-compose.yml:
|
|
Provision the Loki datasource automatically so you don’t need to click through the UI each time:
Create grafana-provisioning/datasources/loki.yml:
|
|
Visualizing Logs with LogQL
LogQL is Loki’s query language — similar to PromQL but for log streams. Basic patterns:
See all logs from a specific container:
{container_name="traefik"}
Find errors across all containers:
{container_name=~".+"} |= "error"
Filter by compose project:
{compose_project="monitoring"} |= "timeout" | json
Count errors per container over 5-minute windows:
sum by (container_name) (count_over_time({container_name=~".+"} |= "error" [5m]))
Parse JSON logs from Traefik and extract status codes:
{container_name="traefik"} | json | status_code >= 500 | line_format "{{.container_name}}: {{.status_code}} {{.request_uri}}"
To build a dashboard in Grafana:
- Add a new dashboard → “Logs” panel
- Query:
{container_name=~".+"} |= "error" | json - Set visualization to “Logs” (built-in log viewer with highlighting)
- Add a “Time series” panel with
rate({container_name=~".+"}[5m])for log volume per container
Log Retention and S3 Storage
For a homelab with 10-20 containers, filesystem storage works fine for about 7-14 days. Beyond that, configure Loki to use MinIO for scalable S3-compatible storage:
Add MinIO to the stack:
|
|
Update loki-config.yml storage block:
|
|
Retention configuration (28-day retention for chunks, 7-day for index):
|
|
Alerting on Log Patterns
Grafana can alert on log patterns using Loki as a data source.
Alert: Error rate exceeds threshold
- Create a Grafana alert rule with the query:
sum(rate({container_name=~".+"} |= "error" [5m])) by (container_name) > 0.1
- Set condition:
WHEN last() OF query (A, 5m, now) IS ABOVE 0.1 - Add notification channel (email, Telegram, Slack)
Alert: Container crash loop detected
count_over_time({container_name=~".+"} != "alive" != "ready" [2m]) > 10
Troubleshooting
Check Alloy logs:
|
|
Verify Loki is running:
|
|
List available labels in Loki:
|
|
Test log push from curl:
|
|
Common issues
- No logs appearing in Grafana — Alloy needs access to the Docker socket (
/var/run/docker.sock). Check the volume mount. - Labels missing — The
loki.source.dockerblock in Alloy must have labels forwarded. Ensure the pipeline chain is complete. - High memory usage — Reduce
max_query_seriesinlimits_configand loweringestion_rate_mb. - Loki refuses old logs — Check
reject_old_samples_max_agein the config. Default is 168h (7 days).
Next Steps
This Loki stack gives you centralized log search, retention policies, and alerting across all Docker containers in your homelab. From here you can:
- Add Prometheus to the same stack and correlate logs with metrics (CPU, memory, disk) in Grafana
- Set up Grafana OnCall for incident management
- Deploy a second Alloy instance on another Docker host and ship to the same Loki
- Replace the filesystem backend with S3 for production-grade retention
The full Compose file and configs are on GitHub. Start with the patterns above and adapt them to your scale — whether that’s 5 containers or 50.