Featured image of post Docker Log Management: Build a Lightweight Log System with Loki + Grafana + Promtail

Docker Log Management: Build a Lightweight Log System with Loki + Grafana + Promtail

Step-by-step guide to setting up a lightweight centralized Docker log management system with Grafana Loki and Promtail — ditch docker logs and scattered log files for visual log analysis and alerts

Introduction

When you manage multiple Docker containers, log viewing quickly turns into a nightmare:

  • Switching between separate docker logs -f terminals
  • Historical logs lost after container restart
  • No cross-container keyword search
  • No timeline, visualization, or alerting

Loki is Grafana Labs’ log aggregation system, designed specifically for Kubernetes and Docker. It uses the same label mechanism as Prometheus. The key difference from Elasticsearch is that Loki indexes labels, not log content — making storage costs extremely low while keeping queries fast.

This guide walks you through setting up Loki + Promtail + Grafana on your VPS for a complete Docker container log management solution.

Architecture Overview

Log data flow:

Container stdout/stderr → Promtail (log collector) → Loki (storage + indexing) → Grafana (query + visualization)
  • Promtail: Runs on each Docker host, reads container log files, adds labels (container name, image name, etc.), pushes to Loki
  • Loki: Log storage backend, indexes logs by labels, supports LogQL query language
  • Grafana: Log query and visualization dashboard, supports alerting

All components are part of the Grafana ecosystem — 100% open-source, free, and resource-efficient.

Prerequisites

Assuming you already have Docker and Docker Compose installed on your VPS. Minimum requirements:

  • 1 CPU core / 1 GB RAM (handles 20+ containers’ logs easily)
  • 20 GB disk (for log storage; adjust retention period as needed)
  • Docker Compose v2+

Step 1: Create the Docker Compose Deployment

We’ll manage Loki, Promtail, and Grafana together in one Compose file.

# docker-compose.yml
version: "3.8"

networks:
  loki:

services:
  # ── Loki Log Storage ──
  loki:
    image: grafana/loki:2.9.6
    container_name: loki
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/local-config.yaml
      - ./loki-data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    networks:
      - loki
    restart: unless-stopped

  # ── Promtail Log Collector ──
  promtail:
    image: grafana/promtail:2.9.6
    container_name: promtail
    volumes:
      - ./promtail-config.yaml:/etc/promtail/config.yaml
      - /var/log:/var/log
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    command: -config.file=/etc/promtail/config.yaml
    networks:
      - loki
    restart: unless-stopped
    depends_on:
      - loki

  # ── Grafana Visualization ──
  grafana:
    image: grafana/grafana:10.4.1
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin    # Change on first login!
      - GF_INSTALL_PLUGINS=
    volumes:
      - ./grafana-data:/var/lib/grafana
    networks:
      - loki
    restart: unless-stopped
    depends_on:
      - loki

Step 2: Configure Loki

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: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

table_manager:
  retention_deletes_enabled: false
  retention_period: 0s

limits_config:
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  max_query_series: 5000

Tip: For single-VPS setups, filesystem storage is sufficient. If disk space is tight, configure retention_period — e.g., 720h for 30 days.

Step 3: Configure Promtail

Create promtail-config.yaml:

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /etc/promtail/positions.yaml  # tracks already-read positions

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: docker
    docker_sd_configs:
      - host: unix:///var/run/docker.sock
        refresh_interval: 5s
    pipeline_stages:
      - docker: {}                              # auto-parse Docker log format
      - labels:
          container_name: ""                     # container name as label
          container_image: ""                    # image name as label
      - regex:
          expression: "^(?P<level>\\w+)\\s"     # extract log level
      - labels:
          level: ""                              # log level as label
    relabel_configs:
      - source_labels: ["__meta_docker_container_name"]
        regex: "/(.*)"
        target_label: "container"
        replacement: "$1"

This configuration auto-discovers all running Docker containers and forwards their stdout/stderr logs to Loki.

Key point: /var/run/docker.sock must be mounted into the Promtail container for auto-discovery to work.

Step 4: Start the Services

# Create data directories
mkdir -p loki-data grafana-data

# Start everything
docker compose up -d

# Verify status
docker compose ps
docker compose logs -f promtail  # confirm log collection works

On first startup, navigate to http://your-vps-ip:3000 in your browser and log in with admin / the password you set.

Step 5: Add Loki as a Grafana Data Source

  1. In Grafana, go to Connections → Data sources
  2. Click Add data source, select Loki
  3. Set URL to http://loki:3100
  4. Click Save & Test — you should see “Data source connected and labels found.”

If you see labels like container_name, container_image, and level, everything is working.

Step 6: Explore Logs with LogQL

Grafana’s Explore panel (🔍 icon) is the primary interface for querying logs.

Basic Queries

# View all logs
{container_name=~".+"}

# Filter by specific container
{container_name="nginx"}

# Multiple containers at once
{container_name=~"nginx|traefik|app-web"}

# Filter by image name
{container_image=~"nginx:.*"}
# Search for errors
{container_name=~".+"} |= "error"

# Exclude health check noise
{container_name="nginx"} != "/health"

# Regex match
{container_name="app"} |~ "Exception|Timeout|Fatal"

# Parse JSON logs and filter by field
{container_name="api"} | json | status >= 400

Metric-style Queries (PromQL-like)

# Logs per minute per container
rate({container_name=~".+"}[1m]) by (container_name)

# Error rate trend
sum by (container_name) (rate({container_name=~".+"} |= "error"[5m]))

Step 7: Set Up Log Alerts

Loki log alerts work similarly to Prometheus metric alerts. Suppose you want to be notified when any container produces too many 5xx errors:

  1. Go to Grafana Alerting → Alert rules → New alert rule
  2. Select the Loki data source
  3. Enter the query:
sum by (container_name) (
  count_over_time(
    {container_name=~".+"} | json | status >= 500
    [5m]
  )
) > 10
  1. Set evaluation interval to 1m, condition WHEN avg() OF query IS ABOVE 10
  2. Configure notification channels (Email, Telegram, Slack, Webhook, etc.)

Now you’ll be alerted whenever any container exceeds 10 5xx errors in a 5-minute window.

Step 8: Add a Domain with Your Reverse Proxy

If you already run Traefik or Nginx, add a subdomain for Grafana:

# Traefik labels example
services:
  grafana:
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`logs.yourdomain.com`)"
      - "traefik.http.routers.grafana.entrypoints=https"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"

Optimization & Maintenance Tips

Log Retention

Loki doesn’t have built-in log deletion, but you can control retention via config. Or use a cron job to clean old data:

# Keep only the last 7 days of log data
find /opt/loki/loki-data -mtime +7 -delete

Resource Usage Reference

ContainersLog VolumeLoki MemoryPromtail Memory
5Light~150 MB~30 MB
20Medium~400 MB~80 MB
50Heavy~1 GB~150 MB

Security Hardening

  • Set a strong Grafana password via GF_SECURITY_ADMIN_PASSWORD
  • Always use HTTPS in production (via reverse proxy)
  • Promtail’s HTTP port (9080) doesn’t need to be public-facing
  • Consider enabling OAuth/OIDC for Grafana authentication

Alternative Solutions Comparison

FeatureLoki + PromtailElasticsearch + FilebeatSelf-hosted Graylog
Resource Usage⭐ Very low🟡 High🟡 Medium
Setup Complexity⭐ Minimal (2 YAMLs)🔴 Complex (JVM tuning)🟡 Moderate
Query LanguageLogQL (PromQL-like)Query DSL (JSON)Lucene queries
Grafana Integration✅ Perfect✅ Supported⚠️ Plugin needed
VPS Suitability⭐ Highly recommended❌ Too resource-heavy🟡 Feasible

Summary

With Loki + Promtail + Grafana, you can set up a complete Docker container log management system in minutes:

  • Loki handles log storage and indexing with minimal resource usage
  • Promtail auto-discovers all Docker containers and collects their logs
  • Grafana provides powerful LogQL queries and visualization dashboards
  • Built-in alerting for anomalies and error spikes

Best of all — everything is free and open-source. No Elastic license restrictions, no log-volume-based billing, no user limits. For lightweight VPS deployments, this is the most cost-effective log management solution available today.

Next steps: Try linking Loki with Prometheus metrics data — view metrics and logs side by side in the same Grafana dashboard for full observability.

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials