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
| Feature | Grafana Loki | ELK Stack |
|---|---|---|
| Minimum RAM | ~200MB | ~2GB+ |
| Indexing method | Labels only | Full-text index |
| Query language | LogQL | Lucene / KQL |
| Grafana integration | Native | Via plugin |
| Installation complexity | Simple | Complex |
Prerequisites
- Linux VPS (Ubuntu 20.04/22.04 or Debian 11/12) with at least 1GB RAM
- Docker and Docker Compose installed
- Grafana already running (or we'll install it together)
- Port 3100 accessible for Loki (or use internal Docker network)
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
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
- Open Grafana at
http://YOUR_IP:3000and log in - Go to Configuration → Data Sources → Add data source
- Select Loki
- Set URL to
http://loki:3100(if using the same Docker network) - 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
- Go to Dashboards → New → New Dashboard
- Click Add visualization
- Select Data source: Loki
- Enter a query such as
{job="nginx"} |= "error" - Choose visualization type: Logs or Time series for rate graphs
- Save the dashboard
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
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.