Grafana Loki Log Aggregation on VPS

📅 October 5, 2026 · 🏷️ Monitoring, Logging, Grafana

Install Grafana Loki on VPS for Log Aggregation

Grafana Loki is an open-source log aggregation system designed to be resource-efficient. Unlike the ELK Stack which indexes all log content, Loki only indexes labels (metadata), dramatically reducing RAM and disk usage. This makes it practical to run a full log aggregation stack even on a small VPS.

Paired with Promtail (the log shipper) and Grafana (visualization), you get a complete observability stack — collect logs from every service, search with LogQL, and build dashboards in minutes.

Loki vs ELK Stack Comparison

FeatureGrafana LokiELK Stack
Minimum RAM~200MB~2GB+
Indexing methodLabels onlyFull-text index
Query languageLogQLLucene / KQL
Grafana integrationNativeVia plugin
Installation complexitySimpleComplex

Prerequisites

Step 1 — Create Directory Structure

# Create main directories
mkdir -p /opt/loki/{config,data}
mkdir -p /opt/promtail/config
cd /opt/loki

Step 2 — Write Loki Configuration

cat > /opt/loki/config/loki-config.yml <<'EOF'
auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096

ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 0s
  chunk_idle_period: 1h
  max_chunk_age: 1h
  chunk_target_size: 1048576
  chunk_retain_period: 30s

schema_config:
  configs:
    - from: 2020-10-24
      store: boltdb-shipper
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 24h

storage_config:
  boltdb_shipper:
    active_index_directory: /loki/boltdb-shipper-active
    cache_location: /loki/boltdb-shipper-cache
    cache_ttl: 24h
    shared_store: filesystem
  filesystem:
    directory: /loki/chunks

compactor:
  working_directory: /loki/boltdb-shipper-compactor
  shared_store: filesystem

limits_config:
  reject_old_samples: true
  reject_old_samples_max_age: 168h
  retention_period: 30d

chunk_store_config:
  max_look_back_period: 0s

table_manager:
  retention_deletes_enabled: true
  retention_period: 720h
EOF

Step 3 — Write Promtail Configuration

cat > /opt/promtail/config/promtail-config.yml <<'EOF'
server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

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

scrape_configs:
  # Collect from systemd journal
  - job_name: journal
    journal:
      max_age: 12h
      labels:
        job: systemd-journal
        host: ${HOSTNAME}
    relabel_configs:
      - source_labels: ['__journal__systemd_unit']
        target_label: 'unit'

  # Collect from Nginx logs
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          host: ${HOSTNAME}
          __path__: /var/log/nginx/*.log

  # Collect from Docker container logs
  - job_name: docker
    static_configs:
      - targets:
          - localhost
        labels:
          job: docker
          host: ${HOSTNAME}
          __path__: /var/lib/docker/containers/*/*-json.log
    pipeline_stages:
      - json:
          expressions:
            output: log
            stream: stream
            attrs:
      - json:
          source: attrs
          expressions:
            tag:
      - regex:
          source: tag
          expression: '^.*/(?P[^/]+)/.*$'
      - labels:
          stream:
          container_name:
      - output:
          source: output
EOF

Step 4 — Create Docker Compose File

cat > /opt/loki/docker-compose.yml <<'EOF'
version: "3.8"

networks:
  loki:
    driver: bridge

services:
  loki:
    image: grafana/loki:2.9.0
    container_name: loki
    restart: unless-stopped
    ports:
      - "127.0.0.1:3100:3100"
    volumes:
      - /opt/loki/config:/etc/loki
      - /opt/loki/data:/loki
    command: -config.file=/etc/loki/loki-config.yml
    networks:
      - loki

  promtail:
    image: grafana/promtail:2.9.0
    container_name: promtail
    restart: unless-stopped
    volumes:
      - /opt/promtail/config:/etc/promtail
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /run/log/journal:/run/log/journal:ro
      - /etc/machine-id:/etc/machine-id:ro
    command: -config.file=/etc/promtail/promtail-config.yml
    environment:
      - HOSTNAME=${HOSTNAME}
    networks:
      - loki

  grafana:
    image: grafana/grafana:10.2.0
    container_name: grafana
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=changeme123
      - GF_USERS_ALLOW_SIGN_UP=false
    volumes:
      - grafana-storage:/var/lib/grafana
    networks:
      - loki

volumes:
  grafana-storage:
EOF

Step 5 — Set Permissions and Start Services

# Set ownership for Loki container (UID 10001)
chown -R 10001:10001 /opt/loki/data

# Start all services
cd /opt/loki
docker compose up -d

# Verify containers are running
docker compose ps
Tip: If you already have Grafana running, remove the grafana service from docker-compose.yml and connect Loki to the same Docker network as your existing Grafana instance.

Step 6 — Add Loki as a Data Source in Grafana

  1. Open Grafana at http://YOUR_IP:3000 and log in
  2. Go to Configuration → Data Sources → Add data source
  3. Select Loki
  4. Set URL to http://loki:3100 (if using the same Docker network)
  5. Click Save & Test — you should see "Data source connected"

Step 7 — Query Logs with LogQL

LogQL is Loki's query language, similar to PromQL for Prometheus. Example queries:

# View all Nginx logs
{job="nginx"}

# Filter logs containing "error"
{job="nginx"} |= "error"

# Count error rate per minute
rate({job="nginx"} |= "error" [1m])

# View logs from a specific Docker container
{job="docker", container_name="myapp"}

# Filter HTTP 500 errors from access log
{job="nginx"} | pattern '<ip> - - [<time>] "<method> <path> <proto>" <status> <size>' | status = "500"

Step 8 — Create a Dashboard in Grafana

  1. Go to Dashboards → New → New Dashboard
  2. Click Add visualization
  3. Select Data source: Loki
  4. Enter a query such as {job="nginx"} |= "error"
  5. Choose visualization type: Logs or Time series for rate graphs
  6. Save the dashboard
Tip: Grafana has pre-built community dashboards for Loki at grafana.com/dashboards. Search for "Loki" and import by dashboard ID — for example, dashboard 13639 for Linux system logs.

Configure Nginx Reverse Proxy for Grafana

cat > /etc/nginx/sites-available/grafana <<'EOF'
server {
    listen 80;
    server_name grafana.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
EOF

ln -s /etc/nginx/sites-available/grafana /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Verify Loki Is Working Correctly

# Check Loki health endpoint
curl http://localhost:3100/ready
# Should respond: ready

# Check Loki metrics
curl http://localhost:3100/metrics | grep loki_ingester

# View Loki container logs
docker logs loki --tail 20

# Check Promtail scrape targets
curl http://localhost:9080/targets
# Lists all configured scrape targets

# Test query via API directly
curl -G -s "http://localhost:3100/loki/api/v1/labels" | python3 -m json.tool
Tip: If Loki is not receiving logs, check Promtail logs with docker logs promtail and verify that the paths in your Promtail config match files that actually exist on the host.

Troubleshooting Common Issues

Error: "too many outstanding requests"

This occurs when Loki receives logs faster than it can process. Increase the ingestion limits in loki-config.yml:

limits_config:
  ingestion_rate_mb: 16
  ingestion_burst_size_mb: 32
  per_stream_rate_limit: 3MB
  per_stream_rate_limit_burst: 15MB

Error: "entry out of order" in Promtail

This happens when log entry timestamps are not in ascending order, often with custom-formatted logs. Add a pipeline stage to parse timestamps correctly:

# In promtail config, add a timestamp pipeline stage
pipeline_stages:
  - regex:
      expression: '.*(?P<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})'
  - timestamp:
      source: timestamp
      format: RFC3339

Error: "context deadline exceeded" when querying

The query takes too long, usually because you're scanning a large time range without a label filter. Always use a label selector first to narrow the search:

# ❌ Slow — scans every log stream
{} |= "error"

# ✅ Fast — filter by label first
{job="nginx"} |= "error"

Promtail not collecting Docker logs

Ensure Docker is using the json-file logging driver (default) and that Promtail has permission to read /var/lib/docker/containers:

# Check container logging driver
docker inspect CONTAINER_NAME | grep LogConfig -A5

# Add promtail to the docker group
usermod -aG docker promtail

# Or in docker-compose.yml, add the docker group GID
group_add:
  - "999"  # docker group GID

Loki disk fills up quickly

Check your retention policy and enable the compactor to automatically delete old logs:

# Check Loki disk usage
du -sh /opt/loki/data/

# In loki-config.yml, configure retention
limits_config:
  retention_period: 720h  # Keep 30 days

compactor:
  working_directory: /loki/boltdb-shipper-compactor
  shared_store: filesystem
  compaction_interval: 10m
  retention_enabled: true
  retention_delete_delay: 2h

Frequently Asked Questions

How is Loki different from Elasticsearch?

Elasticsearch performs full-text indexing on every field, enabling flexible search but consuming significant disk and RAM. Loki only indexes labels (metadata), not log content, reducing resource usage by 10–50x. The trade-off is that content searches use regex/filters which are slower for complex queries. For resource-constrained VPS environments, Loki is almost always the better choice.

What is the difference between Promtail and Filebeat?

Promtail is purpose-built for shipping logs to Loki, with native support for label enrichment and powerful pipeline stages. Filebeat is more flexible and can ship to Elasticsearch, Logstash, and Loki, but requires more complex configuration. For a Loki-based stack, Promtail provides the smoothest integration experience.

Can I run Loki on a 1GB RAM VPS?

Yes. Loki + Promtail together use around 200–400MB of RAM under normal log volumes. If your server handles high traffic (busy web server), allocate at least 2GB RAM and set a shorter retention period (7–14 days) to conserve disk space.