Loki + Promtail + Grafana on a VPS: Centralized Log Aggregation Without Elasticsearch
Loki is Grafana Labs’ log aggregation system — like Prometheus but for logs. Unlike Elasticsearch (which indexes all log content and requires 4–16 GB RAM), Loki only indexes log metadata (labels) and compresses raw log text, making it 10× cheaper to run. Paired with Promtail (log shipper) and Grafana (visualization), the PLG stack provides centralized log aggregation for all services on a VPS with sub-200 MB RAM overhead.
PLG Stack vs ELK Stack
| Factor | Loki (PLG) | Elasticsearch (ELK) |
|---|---|---|
| RAM requirement | ~100–200 MB | 4–8 GB minimum |
| Index approach | Label-based metadata only | Full-text search index |
| Query language | LogQL | Lucene / KQL |
| Grafana integration | Native (same company) | Plugin required |
| Cost to self-host | Low (any VPS) | High (4+ GB RAM) |
| Full-text search | Regex over chunks | Fast full-text index |
| Best for | DevOps, infrastructure logs | Application search, analytics |
Step 1: Docker Compose Setup
<code">mkdir -p /opt/loki/{config,data} && cd /opt/loki
nano docker-compose.yml
<code">version: '3.8'
services:
loki:
image: grafana/loki:latest
container_name: loki
restart: always
ports:
- "127.0.0.1:3100:3100"
command: -config.file=/etc/loki/config.yml
volumes:
- ./config/loki.yml:/etc/loki/config.yml:ro
- loki_data:/loki
promtail:
image: grafana/promtail:latest
container_name: promtail
restart: always
command: -config.file=/etc/promtail/config.yml
volumes:
- ./config/promtail.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
depends_on:
- loki
grafana:
image: grafana/grafana:latest
container_name: grafana-logs
restart: always
ports:
- "127.0.0.1:3001:3000"
environment:
GF_SECURITY_ADMIN_USER: admin
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
GF_AUTH_ANONYMOUS_ENABLED: "false"
volumes:
- grafana_data:/var/lib/grafana
depends_on:
- loki
volumes:
loki_data:
grafana_data:
Step 2: Loki Configuration
<code">nano config/loki.yml
<code">auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_addr: 127.0.0.1
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
limits_config:
retention_period: 30d # Keep 30 days of logs
max_query_series: 10000
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
compactor:
working_directory: /loki/compactor
retention_enabled: true
Step 3: Promtail Configuration
<code">nano config/promtail.yml
<code">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:
# System logs
- job_name: system
static_configs:
- targets: [localhost]
labels:
job: syslog
host: YOUR_HOSTNAME
__path__: /var/log/syslog
# Nginx access logs
- job_name: nginx
static_configs:
- targets: [localhost]
labels:
job: nginx
host: YOUR_HOSTNAME
type: access
__path__: /var/log/nginx/access.log
pipeline_stages:
- regex:
expression: '^(?P[\w\.]+) .* \[(?P[^\]]+)\] "(?P\w+) (?P[^ ]+) [^"]+" (?P\d+) (?P\d+)'
- labels:
method:
status:
# Nginx error logs
- job_name: nginx_errors
static_configs:
- targets: [localhost]
labels:
job: nginx
type: error
host: YOUR_HOSTNAME
__path__: /var/log/nginx/error.log
# Docker container logs
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: container
- source_labels: ['__meta_docker_container_image']
target_label: image
<code">echo "GRAFANA_PASSWORD=StrongGrafanaPassword!" > .env chmod 600 .env docker compose up -d docker compose logs -f loki
Step 4: Nginx for Grafana
<code">sudo nano /etc/nginx/sites-available/grafana-logs
<code">server {
listen 443 ssl http2;
server_name logs.yourdomain.com;
ssl_certificate /etc/letsencrypt/live/logs.yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/logs.yourdomain.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3001;
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;
}
}
<code">sudo certbot --nginx -d logs.yourdomain.com sudo ln -s /etc/nginx/sites-available/grafana-logs /etc/nginx/sites-enabled/ sudo systemctl reload nginx
Step 5: Connect Loki to Grafana
- Login to
https://logs.yourdomain.com - Connections → Data Sources → Add → Loki
- URL:
http://loki:3100 - Save & Test → “Data source connected”
Step 6: LogQL Queries
<code"># Basic log stream (all nginx logs)
{job="nginx"}
# Filter by label (HTTP 5xx errors)
{job="nginx", status="5xx"}
# Filter by content (grep equivalent)
{job="nginx"} |= "ERROR"
# Regex filter
{job="nginx"} |~ "POST /api/.*" | status = "500"
# Rate query (requests per second over time)
rate({job="nginx"}[5m])
# Error rate (5xx per minute)
sum(rate({job="nginx", status=~"5.."}[1m]))
# Count log lines per container
sum by (container) (count_over_time({job="docker"}[1h]))
# Top 10 IP addresses in Nginx logs
topk(10, sum by (remote_addr) (count_over_time({job="nginx"}[1h])))
Step 7: Build Log Dashboard
- Dashboards → New Dashboard → Add Panel
- Select Loki as data source
- Use “Logs” visualization for raw log streams
- Use “Time series” for rate queries (
rate({job="nginx"}[5m])) - Use “Stat” for total error count (
count_over_time({job="nginx"} |= "ERROR" [24h]))
Save as “VPS Log Overview” dashboard — add panels for: Nginx request rate, Error log stream, Docker container logs, Systemd service failures.
Getting Started
The PLG stack (Loki + Promtail + Grafana) uses 200–400 MB total RAM — it runs alongside production services on any Ubuntu VPS at VPS.DO. Loki’s 30-day retention with filesystem storage uses approximately 1–5 GB disk for a typical VPS generating moderate log volume. The label-based indexing means storage scales with label cardinality, not log volume — no need to manage massive Elasticsearch indices.
Conclusion
The Loki + Promtail + Grafana stack provides centralized log aggregation at a fraction of the resource cost of Elasticsearch. Promtail automatically discovers Docker containers and tails log files, Loki stores logs with efficient compression and label-based indexing, and Grafana provides LogQL-powered dashboards for real-time log exploration. For VPS operators who want centralized visibility into all services without the 4 GB RAM Elasticsearch tax, the PLG stack is the practical choice.