Configuring Centralized Log Aggregation with Grafana Loki

What You’ll Need
- Hetzner VPS or Contabo VPS for hosting
- DigitalOcean as an alternative hosting provider
- Namecheap if you need a custom domain for Grafana ingress
- Docker Engine (v24.0+) and Docker Compose installed on your server
- Ubuntu 22.04 LTS host with a non-root user possessing sudo privileges
Table of Contents
- Understanding Loki Architecture for Self-Hosted Logs
- Step 1: Deploying Grafana Loki and Grafana via Docker Compose
- Step 2: Configuring Promtail to Collect Docker and System Logs
- Step 3: Querying and Visualizing Logs with LogQL in Grafana
- Getting Started
Understanding Loki Architecture for Self-Hosted Logs
When I first migrated my production infrastructure away from expensive third-party logging platforms, I quickly realized that traditional ElasticSearch stacks were resource hogs. Storing complete inverted indexes of raw log strings burns through gigabytes of RAM instantly. If you are looking at How to Reduce Your SaaS Bill with Self-Hosted Alternatives, replacing commercial observability platforms with Grafana Loki is one of the highest leverage moves you can make.
Grafana Loki takes a fundamentally different approach compared to ELK or OpenSearch. Inspired by Prometheus, Loki does not index the text of your log lines. Instead, it assigns key-value labels to log streams and indexes only those labels. The raw log messages are compressed and stored as chunks in local filesystems or object storage. This decision slashes operational overhead and resource consumption by orders of magnitude.
The stack consists of three primary components working together:
- Promtail: The log collector and shipper. It tail-reads files, attaches labels from service discovery or manual configs, and pushes log entries to Loki over HTTP/gRPC APIs.
- Loki: The core ingestion and storage engine. It validates chunks, writes indexes using TSDB structures, and handles LogQL queries.
- Grafana: The visualization frontend. It queries Loki using LogQL to present dashboards, trigger alerts, and allow deep exploration during incidents.
Because Loki indexes metadata rather than content, label discipline is crucial. High cardinality (like adding unique user IDs or millisecond timestamps as labels) can destroy Loki performance. Keep your labels scoped to environment names, app names, or container names, and let Loki grep the message payload on-the-fly during queries.
Step 1: Deploying Grafana Loki and Grafana via Docker Compose
To get started, provision a cloud instance such as a Hetzner VPS or a DigitalOcean droplet. I recommend a server with at least 2 CPU cores and 4 GB of RAM to smoothly process high log throughput.
First, create a dedicated directory on your server to house the configuration files and persistent volume mounts. Run these commands on your terminal:
mkdir -p /opt/logging-stack
cd /opt/logging-stack
Now, create the primary configuration file for Grafana Loki named loki-config.yaml. This file defines server ports, storage mechanisms, retention policies, and schema structures.
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
common:
instance_id: index_1
path_prefix: /tmp/loki
storage:
filesystem:
chunks_directory: /tmp/loki/chunks
rules_directory: /tmp/loki/rules
replication_factor: 1
ring:
kvstore:
store: inmemory
query_range:
results_cache:
cache:
embedded_cache:
enabled: true
max_size_mb: 100
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
Next, create the docker-compose.yml file to orchestrate Grafana, Loki, and the underlying network.
version: '3.8'
networks:
logging-net:
driver: bridge
services:
loki:
image: grafana/loki:2.9.2
container_name: logging_loki
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml
- loki_data:/tmp/loki
command: -config.file=/etc/loki/loki-config.yaml
networks:
- logging-net
restart: unless-stopped
grafana:
image: grafana/grafana:10.2.2
container_name: logging_grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=SuperSecretPassword123!
volumes:
- grafana_data:/var/lib/grafana
networks:
- logging-net
restart: unless-stopped
volumes:
loki_data:
grafana_data:
Start Loki and Grafana by executing:
docker compose up -d
Verify that both containers are running properly using docker compose ps. Loki will begin listening on HTTP port 3100, while Grafana is accessible on port 3000.
💡 Fast-Track Your Project: Don’t want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-HUGO.
Step 2: Configuring Promtail to Collect Docker and System Logs
With the backend storage and user interface running, we need a shipping agent to gather logs. Promtail runs locally on each host machine, hooks into system log files and Docker daemon sockets, and streams formatted entries directly to Loki.
Create a file named promtail-config.yaml inside your /opt/logging-stack folder:
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:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: syslog
__path__: /var/log/*.log
- 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_log_stream']
target_label: 'stream'
- source_labels: ['__meta_docker_container_label_logging_job']
target_label: 'job'
Notice how docker_sd_configs dynamically discovers containers using the local Docker daemon socket. The relabel_configs block cleans up container names, turning raw internal strings into clean labels like container="logging_grafana".
Now update your docker-compose.yml to include the Promtail service:
version: '3.8'
networks:
logging-net:
driver: bridge
services:
loki:
image: grafana/loki:2.9.2
container_name: logging_loki
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/loki-config.yaml
- loki_data:/tmp/loki
command: -config.file=/etc/loki/loki-config.yaml
networks:
- logging-net
restart: unless-stopped
promtail:
image: grafana/promtail:2.9.2
container_name: logging_promtail
volumes:
- ./promtail-config.yaml:/etc/promtail/promtail-config.yaml
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
command: -config.file=/etc/promtail/promtail-config.yaml
networks:
- logging-net
restart: unless-stopped
grafana:
image: grafana/grafana:10.2.2
container_name: logging_grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=SuperSecretPassword123!
volumes:
- grafana_data:/var/lib/grafana
networks:
- logging-net
restart: unless-stopped
volumes:
loki_data:
grafana_data:
Launch the updated stack:
docker compose up -d
Promtail immediately mounts /var/run/docker.sock and begins tailing log entries from every running container. This automatic setup shines when managing complex server infrastructure. For example, if you are Setting Up PostgreSQL Connection Pooling with PgBouncer, Promtail will capture connection logs, timeout errors, and pool saturation events from both database and proxy containers without requiring separate software agents.
Step 3: Querying and Visualizing Logs with LogQL in Grafana
Now that logs are streaming into Loki, open your web browser and navigate to http://YOUR_SERVER_IP:3000. Log in using admin and the password specified in your Docker Compose file (SuperSecretPassword123!).
To add Loki as an active data source:
- Navigate to Connections > Data Sources > Add data source.
- Select Loki.
- Set the URL to
http://loki:3100(leveraging Docker’s internal DNS network). - Click Save & test. Grafana will display a green success notification.
Now click on Explore in the left sidebar to run your first LogQL query.
LogQL uses stream selectors enclosed in curly braces followed by filter expressions. Here are practical LogQL examples for real-world operations.
Stream Filtering
To fetch logs from all Docker containers managed by Promtail:
{job="docker"}
To narrow down results to a single container, specify the container label:
{container="logging_grafana"}
Line Filters
You can apply plain text or regex filter expressions to the raw log payload using line filter operators:
|=Line contains string!=Line does not contain string~=Line matches regular expression!~Line does not match regular expression
To search for any error logs across all running Docker containers, execute:
{job="docker"} |= "error" != "debug"
Parsing JSON Payloads and Metrics Aggregation
If your microservices or API automation tools emit JSON-formatted logs, Loki can extract log properties on the fly. When building complex automated flows, contrasting execution patterns between approaches like Temporal vs Make for API-First Workflows shows how useful centralized JSON logging is. Structured logs let you trace workflow execution steps instantly.
To parse JSON fields dynamically and filter by HTTP status code:
{container="api_gateway"} | json | status = "500"
You can also calculate metrics from log lines over time. To compute the per-second rate of error logs averaged over 5-minute windows across containers:
sum(rate({job="docker"} |= "error" [5m])) by (container)
Adding this rate metric query to a Grafana Dashboard panel gives you a real-time system error counter chart that auto-updates without taxing server resources.
Getting Started
Centralized logging is a core requirement for reliable server management. By pairing Grafana Loki with Promtail, you eliminate reliance on expensive SaaS log monitoring platforms while retaining rapid search, dashboard capabilities, and log alerting.
If you are ready to deploy your own log aggregation stack:
- Spin up a fresh Linux instance on a host like Hetzner VPS or DigitalOcean.
- Install Docker, clone the
docker-compose.yml,loki-config.yaml, andpromtail-config.yamlfiles presented in this guide. - Hook up Grafana, set up LogQL queries, and build your centralized monitoring dashboards.
Outsource Your Automation
Don’t have time? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines. Hire me on Fiverr, mention SYS3-HUGO for priority. Or DM at chasebot.online.
Want to automate this yourself?
Start with n8n Cloud (free tier available) or self-host on a Hetzner VPS for full control.
Want this engine running on your own VPS?
This blog publishes itself — daily, unattended, on free API tiers. The full engine, Hugo theme, and setup guide are available as System 3.
Get System 3