Set up Grafana Loki and Promtail to collect, aggregate, and analyze logs from Docker containers. Configure retention policies, integrate with Grafana for visualization, and enable real-time log monitoring across your infrastructure.
Prerequisites
- Docker and Docker Compose installed
- Minimum 2GB RAM available
- At least 10GB free disk space for log storage
What this solves
Loki provides centralized log aggregation for Docker containers without the resource overhead of traditional solutions like Elasticsearch. Promtail acts as the log collection agent, automatically discovering and shipping container logs to Loki, where you can query and analyze them through Grafana dashboards.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you have the latest security patches.
sudo apt update && sudo apt upgrade -y
Install Docker and Docker Compose
Install Docker and Docker Compose to run Loki, Promtail, and test containers.
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker $USER
newgrp docker
Create project directory structure
Create a dedicated directory structure for Loki configuration files and data storage.
mkdir -p ~/loki-stack/{config,data/loki,data/grafana}
cd ~/loki-stack
Configure Loki server
Create the main Loki configuration file with retention policies and storage settings.
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
instance_addr: 127.0.0.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: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
ruler:
alertmanager_url: http://localhost:9093
Retention configuration
limits_config:
retention_period: 168h # 7 days
max_query_length: 12000h
max_query_parallelism: 16
max_streams_per_user: 10000
max_line_size: 256000
Compactor for log retention
compactor:
working_directory: /tmp/loki/retention
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem
Configure Promtail for Docker log collection
Create Promtail configuration to automatically discover and collect Docker container logs.
server:
http_listen_port: 9080
grpc_listen_port: 0
log_level: info
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
tenant_id: docker
scrape_configs:
# Docker container logs
- job_name: docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
filters:
- name: label
values: ["logging=promtail"]
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_com_docker_compose_service']
target_label: service
- source_labels: ['__meta_docker_container_label_com_docker_compose_project']
target_label: project
pipeline_stages:
- docker: {}
- timestamp:
source: time
format: RFC3339Nano
- output:
source: output
# System logs
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*.log
pipeline_stages:
- match:
selector: '{job="varlogs"}'
stages:
- timestamp:
source: timestamp
format: Jan 02 15:04:05
- regex:
expression: '^(?P\w+ \d+ \d+:\d+:\d+) (?P\w+) (?P\w+)(\[(?P\d+)\])?: (?P.*)'
- labels:
hostname:
service:
pid:
# Application logs
- job_name: app_logs
static_configs:
- targets:
- localhost
labels:
job: app
__path__: /var/log/app/*.log
pipeline_stages:
- json:
expressions:
level: level
timestamp: timestamp
message: message
- timestamp:
source: timestamp
format: 2006-01-02T15:04:05.000Z
- labels:
level:
Create Docker Compose configuration
Set up the complete monitoring stack with Loki, Promtail, and Grafana in a single compose file.
version: '3.8'
networks:
loki-net:
driver: bridge
volumes:
grafana-data:
driver: local
loki-data:
driver: local
services:
loki:
image: grafana/loki:2.9.2
container_name: loki
restart: unless-stopped
networks:
- loki-net
ports:
- "3100:3100"
volumes:
- ./config/loki.yml:/etc/loki/local-config.yaml:ro
- loki-data:/tmp/loki
command: -config.file=/etc/loki/local-config.yaml
healthcheck:
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3100/ready || exit 1"]
interval: 30s
timeout: 10s
retries: 3
labels:
logging: "promtail"
promtail:
image: grafana/promtail:2.9.2
container_name: promtail
restart: unless-stopped
networks:
- loki-net
ports:
- "9080:9080"
volumes:
- ./config/promtail.yml:/etc/promtail/config.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
depends_on:
loki:
condition: service_healthy
labels:
logging: "promtail"
grafana:
image: grafana/grafana:10.2.0
container_name: grafana
restart: unless-stopped
networks:
- loki-net
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
- ./config/grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml:ro
- ./config/grafana-dashboards.yml:/etc/grafana/provisioning/dashboards/dashboards.yml:ro
- ./dashboards:/var/lib/grafana/dashboards:ro
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
- GF_USERS_ALLOW_SIGN_UP=false
- GF_LOG_LEVEL=info
- GF_INSTALL_PLUGINS=grafana-clock-panel
labels:
logging: "promtail"
# Test application for log generation
test-app:
image: nginx:alpine
container_name: test-nginx
restart: unless-stopped
networks:
- loki-net
ports:
- "8080:80"
labels:
logging: "promtail"
service: "nginx"
environment: "test"
Configure Grafana data source
Create Grafana configuration to automatically add Loki as a data source.
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: true
jsonData:
maxLines: 1000
derivedFields:
- datasourceUid: prometheus
matcherRegex: "traceID=(\\w+)"
name: TraceID
url: "http://localhost:3000/explore?left=[\"now-1h\",\"now\",\"Jaeger\",{\"query\":\"$${__value.raw}\"}]"
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
editable: true
Set up dashboard provisioning
Configure Grafana to automatically load dashboards for log analysis.
apiVersion: 1
providers:
- name: 'Loki Dashboards'
orgId: 1
folder: 'Loki'
type: file
disableDeletion: false
updateIntervalSeconds: 30
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards
Create dashboard directory and sample dashboard
Create a basic Loki dashboard for container log visualization.
mkdir -p ~/loki-stack/dashboards
{
"dashboard": {
"id": null,
"title": "Docker Container Logs",
"tags": ["loki", "docker", "logs"],
"timezone": "browser",
"time": {
"from": "now-1h",
"to": "now"
},
"panels": [
{
"id": 1,
"title": "Container Logs Stream",
"type": "logs",
"targets": [
{
"expr": "{container=~\".+\"}",
"refId": "A"
}
],
"gridPos": {
"h": 12,
"w": 24,
"x": 0,
"y": 0
},
"options": {
"showTime": true,
"showLabels": true,
"showCommonLabels": false,
"wrapLogMessage": true,
"sortOrder": "Descending"
}
},
{
"id": 2,
"title": "Log Volume by Container",
"type": "stat",
"targets": [
{
"expr": "sum by (container) (count_over_time({container=~\".+\"}[5m]))",
"refId": "A"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 12
}
},
{
"id": 3,
"title": "Error Rate",
"type": "stat",
"targets": [
{
"expr": "sum(rate({container=~\".+\"} |~ \"(?i)error\")[5m])) * 100",
"refId": "A"
}
],
"gridPos": {
"h": 8,
"w": 12,
"x": 12,
"y": 12
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"thresholds": {
"steps": [
{
"color": "green",
"value": null
},
{
"color": "yellow",
"value": 5
},
{
"color": "red",
"value": 10
}
]
},
"unit": "percent"
}
}
}
],
"refresh": "5s",
"version": 1
}
}
Set correct file permissions
Ensure proper ownership and permissions for configuration files and data directories.
sudo chown -R $USER:$USER ~/loki-stack
chmod -R 755 ~/loki-stack/config
chmod 644 ~/loki-stack/config/*.yml
chmod 644 ~/loki-stack/docker-compose.yml
Start the Loki stack
Launch all services using Docker Compose with proper startup ordering.
cd ~/loki-stack
docker-compose up -d
Configure log retention cleanup job
Set up a systemd timer to periodically clean up old logs and maintain disk space.
sudo mkdir -p /etc/systemd/system
[Unit]
Description=Loki Log Cleanup Service
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
User=root
ExecStart=/bin/bash -c 'docker exec loki /usr/bin/loki -config.file=/etc/loki/local-config.yaml -target=compactor -dry-run=false'
RemainAfterExit=no
[Install]
WantedBy=multi-user.target
[Unit]
Description=Run Loki cleanup daily
Requires=loki-cleanup.service
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now loki-cleanup.timer
Configure advanced log processing
Set up log parsing for structured logs
Configure Promtail to parse JSON logs and extract structured data for better querying.
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: docker-json
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
filters:
- name: label
values: ["logging=promtail"]
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)
target_label: container
- source_labels: ['__meta_docker_container_label_com_docker_compose_service']
target_label: service
pipeline_stages:
- docker: {}
- json:
expressions:
level: level
timestamp: timestamp
message: message
service: service
trace_id: trace_id
- timestamp:
source: timestamp
format: RFC3339Nano
- labels:
level:
service:
trace_id:
- output:
source: message
Configure alerting rules
Create alerting rules for log-based monitoring and incident response.
groups:
- name: loki_alerts
rules:
- alert: HighErrorRate
expr: |
sum(rate({container=~".+"} |~ "(?i)error" [5m])) by (container) > 0.1
for: 2m
labels:
severity: warning
team: platform
annotations:
summary: "High error rate detected in container {{ $labels.container }}"
description: "Container {{ $labels.container }} is generating errors at {{ $value }} errors per second for more than 2 minutes."
- alert: ContainerLogVolumeHigh
expr: |
sum(rate({container=~".+"}[5m])) by (container) > 100
for: 5m
labels:
severity: info
team: platform
annotations:
summary: "High log volume from container {{ $labels.container }}"
description: "Container {{ $labels.container }} is generating {{ $value }} log lines per second."
- alert: ServiceDown
expr: |
absent_over_time({container=~".+"}[10m])
for: 1m
labels:
severity: critical
team: platform
annotations:
summary: "No logs received from container {{ $labels.container }}"
description: "Container {{ $labels.container }} has not generated any logs for over 10 minutes."
Integrate with Grafana alerting
Configure Grafana alerting
Set up Grafana to create alerts based on Loki queries and send notifications.
[alerting]
enabled = true
execute_alerts = true
[unified_alerting]
enabled = true
[smtp]
enabled = false
host = smtp.example.com:587
user = alerts@example.com
password = your_smtp_password
skip_verify = false
from_address = alerts@example.com
from_name = Grafana Alerts
Update Docker Compose with Grafana config
Mount the custom Grafana configuration to enable alerting features.
docker-compose down
Add this volume mount to grafana service in docker-compose.yml
Under volumes section for grafana:
- ./config/grafana.ini:/etc/grafana/grafana.ini:ro
sed -i '/grafana-data:\/var\/lib\/grafana/a\ - ./config/grafana.ini:/etc/grafana/grafana.ini:ro' docker-compose.yml
docker-compose up -d
Verify your setup
Check service status
Verify all components are running and healthy.
docker-compose ps
docker-compose logs loki
docker-compose logs promtail
docker-compose logs grafana
Test log collection
Generate test logs and verify they appear in Loki.
# Generate some test logs
docker exec test-nginx sh -c 'echo "Test log entry $(date)" >> /var/log/nginx/access.log'
curl http://localhost:8080
Query Loki API directly
curl -G -s "http://localhost:3100/loki/api/v1/query" \
--data-urlencode 'query={container="test-nginx"}' \
--data-urlencode 'limit=10' | jq '.data.result[].values[]'
Check Promtail metrics
curl -s http://localhost:9080/metrics | grep promtail_targets_active_total
Verify Grafana integration
Access Grafana and confirm Loki data source is working.
# Access Grafana at http://localhost:3000
Login: admin / admin123
Test Loki queries in Explore view:
echo "Access Grafana at: http://localhost:3000"
echo "Username: admin"
echo "Password: admin123"
echo "Navigate to Explore -> Loki and try query: {container=\"test-nginx\"}"
Check health endpoints
curl -s http://localhost:3100/ready
curl -s http://localhost:3000/api/health
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Loki service fails to start | Configuration syntax error | Run docker logs loki and check config syntax |
| No logs appearing in Loki | Promtail can't access Docker socket | Ensure /var/run/docker.sock is mounted and accessible |
| Permission denied on log files | Promtail can't read system logs | Check file permissions: ls -la /var/log/ |
| Grafana can't connect to Loki | Network connectivity issue | Verify services are on same Docker network: docker network ls |
| High memory usage | Too many log streams | Increase max_streams_per_user or implement log filtering |
| Retention not working | Compactor not running | Check compactor logs and ensure retention is enabled in config |
| Slow query performance | Too broad time range | Use more specific label filters and shorter time ranges |
chown and use minimal permissions like chmod 644 for files or chmod 755 for directories.Next steps
- Configure Loki with S3 storage backend for scalable centralized logging
- Implement Grafana advanced alerting with webhooks and notification channels
- Set up Prometheus Alertmanager webhook notifications for Loki log alerts with Grafana integration
- Configure NGINX log analysis with Loki and Grafana for centralized monitoring
- Configure Fluentd with Kubernetes DaemonSet and log routing for centralized collection
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Variables
LOKI_STACK_DIR="${HOME}/loki-stack"
LOKI_VERSION="2.9.4"
PROMTAIL_VERSION="2.9.4"
# Functions
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
cleanup_on_error() {
log_error "Installation failed. Cleaning up..."
cd "${HOME}" || true
rm -rf "${LOKI_STACK_DIR}" || true
docker-compose -f "${LOKI_STACK_DIR}/docker-compose.yml" down --volumes --remove-orphans 2>/dev/null || true
}
trap cleanup_on_error ERR
usage() {
echo "Usage: $0 [--retention-days DAYS] [--grafana-port PORT]"
echo " --retention-days: Log retention period in days (default: 7)"
echo " --grafana-port: Grafana web port (default: 3000)"
exit 1
}
# Parse arguments
RETENTION_DAYS=7
GRAFANA_PORT=3000
while [[ $# -gt 0 ]]; do
case $1 in
--retention-days)
RETENTION_DAYS="$2"
shift 2
;;
--grafana-port)
GRAFANA_PORT="$2"
shift 2
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
# Check prerequisites
echo "[1/8] Checking prerequisites..."
if [[ $EUID -eq 0 ]]; then
log_error "This script should not be run as root"
exit 1
fi
if ! command -v sudo &> /dev/null; then
log_error "sudo is required but not installed"
exit 1
fi
# Detect distro
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
DOCKER_REPO_SETUP="curl -fsSL https://get.docker.com | sudo sh"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
DOCKER_REPO_SETUP="sudo dnf install -y dnf-plugins-core && sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo && sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
DOCKER_REPO_SETUP="sudo yum install -y docker && sudo yum install -y docker-compose"
;;
*)
log_error "Unsupported distro: $ID"
exit 1
;;
esac
else
log_error "Cannot detect OS distribution"
exit 1
fi
log_info "Detected OS: $PRETTY_NAME"
# Update system
echo "[2/8] Updating system packages..."
sudo bash -c "$PKG_UPDATE"
# Install Docker
echo "[3/8] Installing Docker and Docker Compose..."
if ! command -v docker &> /dev/null; then
sudo bash -c "$DOCKER_REPO_SETUP"
sudo systemctl enable docker --now 2>/dev/null || true
sudo usermod -aG docker "$USER"
newgrp docker
fi
if ! command -v docker-compose &> /dev/null && ! docker compose version &> /dev/null; then
if [[ "$PKG_MGR" == "apt" ]]; then
sudo $PKG_INSTALL docker-compose-plugin
fi
fi
# Create directory structure
echo "[4/8] Creating project directory structure..."
mkdir -p "${LOKI_STACK_DIR}/config"
mkdir -p "${LOKI_STACK_DIR}/data/loki"
mkdir -p "${LOKI_STACK_DIR}/data/grafana"
chmod 755 "${LOKI_STACK_DIR}"
chmod 755 "${LOKI_STACK_DIR}/config"
chmod 755 "${LOKI_STACK_DIR}/data"
chmod 755 "${LOKI_STACK_DIR}/data/loki"
chmod 755 "${LOKI_STACK_DIR}/data/grafana"
cd "${LOKI_STACK_DIR}"
# Create Loki configuration
echo "[5/8] Creating Loki configuration..."
cat > config/loki.yml << 'EOF'
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
common:
instance_addr: 127.0.0.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: 2020-10-24
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h
limits_config:
retention_period: RETENTION_HOURS
max_query_length: 12000h
max_query_parallelism: 16
max_streams_per_user: 10000
max_line_size: 256000
compactor:
working_directory: /tmp/loki/retention
retention_enabled: true
retention_delete_delay: 2h
delete_request_store: filesystem
EOF
# Replace retention placeholder
RETENTION_HOURS=$((RETENTION_DAYS * 24))
sed -i "s/RETENTION_HOURS/${RETENTION_HOURS}h/g" config/loki.yml
# Create Promtail configuration
echo "[6/8] Creating Promtail configuration..."
cat > config/promtail.yml << 'EOF'
server:
http_listen_port: 9080
grpc_listen_port: 0
log_level: info
positions:
filename: /tmp/positions.yaml
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
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.+)'
target_label: container
- source_labels: ['__meta_docker_container_log_stream']
target_label: stream
pipeline_stages:
- docker: {}
EOF
# Create Docker Compose file
echo "[7/8] Creating Docker Compose configuration..."
cat > docker-compose.yml << EOF
version: '3.8'
services:
loki:
image: grafana/loki:${LOKI_VERSION}
container_name: loki
ports:
- "3100:3100"
volumes:
- ./config/loki.yml:/etc/loki/local-config.yaml:ro
- ./data/loki:/tmp/loki
command: -config.file=/etc/loki/local-config.yaml
restart: unless-stopped
networks:
- loki
promtail:
image: grafana/promtail:${PROMTAIL_VERSION}
container_name: promtail
volumes:
- ./config/promtail.yml:/etc/promtail/config.yml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
restart: unless-stopped
networks:
- loki
depends_on:
- loki
grafana:
image: grafana/grafana:latest
container_name: grafana
ports:
- "${GRAFANA_PORT}:3000"
volumes:
- ./data/grafana:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
restart: unless-stopped
networks:
- loki
networks:
loki:
driver: bridge
EOF
chmod 644 config/loki.yml config/promtail.yml docker-compose.yml
# Start services
echo "[8/8] Starting Loki stack..."
docker-compose up -d
# Verification
log_info "Verifying installation..."
sleep 10
if docker-compose ps | grep -q "Up"; then
log_info "✓ Docker containers are running"
else
log_error "✗ Some containers failed to start"
docker-compose logs
exit 1
fi
# Test Loki API
if curl -s http://localhost:3100/ready | grep -q "ready"; then
log_info "✓ Loki is ready and responding"
else
log_warn "⚠ Loki may still be starting up"
fi
# Configure firewall if needed
if command -v ufw &> /dev/null && sudo ufw status | grep -q "Status: active"; then
sudo ufw allow 3100/tcp comment "Loki"
sudo ufw allow "${GRAFANA_PORT}/tcp" comment "Grafana"
fi
if command -v firewall-cmd &> /dev/null; then
sudo firewall-cmd --permanent --add-port=3100/tcp --add-port="${GRAFANA_PORT}/tcp" 2>/dev/null || true
sudo firewall-cmd --reload 2>/dev/null || true
fi
log_info "Installation completed successfully!"
echo ""
echo "Services:"
echo " Loki: http://localhost:3100"
echo " Grafana: http://localhost:${GRAFANA_PORT} (admin/admin)"
echo " Promtail: http://localhost:9080"
echo ""
echo "To add Loki as a data source in Grafana:"
echo " URL: http://loki:3100"
echo ""
echo "Log retention: ${RETENTION_DAYS} days"
echo "Project directory: ${LOKI_STACK_DIR}"
Review the script before running. Execute with: bash install.sh