Self-Hosting Grafana Loki with Docker Compose
What Is Grafana Loki?
Loki is a horizontally scalable, multi-tenant log aggregation system designed by Grafana Labs. Unlike Elasticsearch which indexes full log text, Loki only indexes metadata labels — making it dramatically cheaper to run. It’s designed to work with Grafana for visualization and uses a query language (LogQL) similar to PromQL. It replaces ELK stack (Elasticsearch, Logstash, Kibana), Splunk, and Datadog Logs. Official site
Why Loki over ELK?
| Aspect | Loki | Elasticsearch/ELK |
|---|---|---|
| RAM usage | ~200 MB | 2-4 GB minimum |
| Indexing | Labels only (lightweight) | Full-text (heavy) |
| Storage cost | Low (compressed chunks) | High (inverted indices) |
| Setup complexity | 3 containers | 5+ containers |
| Query language | LogQL (PromQL-like) | KQL / Lucene |
| Best for | Operational logs, homelab | Full-text search, analytics |
For homelabs and small deployments, Loki uses 10-20x less RAM than Elasticsearch for the same log volume.
Prerequisites
- A Linux server (Ubuntu 22.04+ recommended)
- Docker and Docker Compose installed (guide)
- 1 GB of free RAM (Loki + Promtail + Grafana)
- Services generating logs you want to aggregate
Docker Compose Configuration
The standard Loki stack has three components:
services:
loki:
image: grafana/loki:3.4.2
container_name: loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml:ro
- loki-data:/loki
networks:
- loki
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3100/ready"]
interval: 15s
timeout: 5s
retries: 3
promtail:
image: grafana/promtail:3.4.2
container_name: promtail
command: -config.file=/etc/promtail/config.yaml
volumes:
- ./promtail-config.yaml:/etc/promtail/config.yaml:ro
- /var/log:/var/log:ro # Host system logs
- /var/lib/docker/containers:/var/lib/docker/containers:ro # Docker container logs
- promtail-positions:/tmp
networks:
- loki
depends_on:
loki:
condition: service_healthy
restart: unless-stopped
grafana:
image: grafana/grafana:11.5.2
container_name: grafana
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: change-this-password # CHANGE THIS
GF_PATHS_PROVISIONING: /etc/grafana/provisioning
volumes:
- grafana-data:/var/lib/grafana
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/loki.yaml:ro
networks:
- loki
depends_on:
- loki
restart: unless-stopped
networks:
loki:
volumes:
loki-data:
promtail-positions:
grafana-data:
Loki Configuration
Create loki-config.yaml:
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
schema_config:
configs:
- from: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: 30d # Keep logs for 30 days
max_query_length: 720h
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
retention_delete_delay: 2h
retention_delete_worker_count: 150
Promtail Configuration
Create promtail-config.yaml:
server:
http_listen_port: 9080
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
# Scrape Docker container logs
- job_name: docker
static_configs:
- targets:
- localhost
labels:
job: docker
__path__: /var/lib/docker/containers/*/*-json.log
pipeline_stages:
- docker: {}
- labeldrop:
- filename
# Scrape system logs
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/syslog
# Scrape auth logs
- job_name: auth
static_configs:
- targets:
- localhost
labels:
job: auth
__path__: /var/log/auth.log
Grafana Datasource
Create grafana-datasources.yaml:
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
Start the Stack
docker compose up -d
Access Grafana at http://your-server:3000 (default: admin / your configured password). Loki is pre-configured as a datasource.
Querying Logs with LogQL
In Grafana → Explore → select Loki datasource. LogQL examples:
# All logs from a specific container
{job="docker"} |= "nginx"
# Error logs only
{job="docker"} |= "error" != "404"
# JSON log parsing
{job="docker"} | json | status >= 500
# Rate of errors per minute
rate({job="docker"} |= "error" [1m])
# Top 10 noisiest containers
topk(10, sum by (container_name) (rate({job="docker"}[5m])))
Adding Docker Labels for Better Filtering
By default, Promtail groups all Docker logs together. Add labels for per-container filtering:
# In promtail-config.yaml, update the docker scrape config:
scrape_configs:
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
target_label: 'container'
- source_labels: ['__meta_docker_container_log_stream']
target_label: 'logstream'
Mount the Docker socket in Promtail:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Now you can query by container name: {container="nextcloud"}.
Retention and Storage
Loki’s storage is efficient but grows with log volume. Key tuning:
| Setting | Default | Recommendation |
|---|---|---|
retention_period | None (infinite) | 30d for homelabs, 90d for production |
compaction_interval | 10m | Keep default |
| Chunk encoding | Snappy | Default is fine |
Estimate storage: ~1 GB per day for a moderately active homelab (10-20 containers). Set retention to manage growth.
Reverse Proxy
Behind Nginx Proxy Manager:
Proxy Host: logs.example.com → http://grafana:3000
Enable: WebSocket Support, Force SSL
Do NOT expose Loki’s port (3100) publicly — it has no authentication. Access it through Grafana only.
Backup
| Volume | Contains | Priority |
|---|---|---|
loki-data | All ingested logs, indices | Important |
grafana-data | Dashboards, datasources, users | Important |
promtail-positions | Read positions (regenerated) | Low |
See Backup Strategy.
Troubleshooting
Grafana shows “No data” for Loki queries
Symptom: Loki datasource is green but queries return nothing.
Fix: Check Promtail is running and connected: docker logs promtail. Verify Promtail can read log files (check volume mounts). Verify time range in Grafana (default “last 6 hours” may not have data yet).
Loki OOM killed
Symptom: Loki container restarts with exit code 137.
Fix: Loki is loading too many chunks into memory. Add these limits to loki-config.yaml:
limits_config:
ingestion_rate_mb: 10
ingestion_burst_size_mb: 20
max_streams_per_user: 10000
Promtail permission denied on Docker logs
Symptom: Promtail logs show “permission denied” for container log files.
Fix: Ensure the Docker container log directory is mounted with :ro and Promtail runs as root (default in the official image). On SELinux systems, add :z to volume mounts.
Resource Requirements
- Loki: ~200 MB RAM idle, scales with query load
- Promtail: ~50 MB RAM
- Grafana: ~150 MB RAM
- Total stack: ~400 MB idle
- Disk: ~1 GB/day for moderate log volume (10-20 containers)
Verdict
Loki is the right choice for self-hosters who already use Grafana for monitoring or want log aggregation without Elasticsearch’s resource appetite. The LogQL query language is powerful once you learn it, and the label-based approach keeps storage costs low. If you need full-text search across all logs (searching for specific error strings across millions of lines), Elasticsearch is still better. For everything else — operational logging, debugging, alerting on log patterns — Loki does the job at a fraction of the resource cost.
Related
Get self-hosting tips in your inbox
New guides, comparisons, and setup tutorials — delivered weekly. No spam.