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?

AspectLokiElasticsearch/ELK
RAM usage~200 MB2-4 GB minimum
IndexingLabels only (lightweight)Full-text (heavy)
Storage costLow (compressed chunks)High (inverted indices)
Setup complexity3 containers5+ containers
Query languageLogQL (PromQL-like)KQL / Lucene
Best forOperational logs, homelabFull-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:

SettingDefaultRecommendation
retention_periodNone (infinite)30d for homelabs, 90d for production
compaction_interval10mKeep default
Chunk encodingSnappyDefault 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

VolumeContainsPriority
loki-dataAll ingested logs, indicesImportant
grafana-dataDashboards, datasources, usersImportant
promtail-positionsRead 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.