Quản Lý Log VPS Với Loki + Grafana 2026 — Không Còn Mò Mẫm SSH Từng Server Để Đọc Log
1. 3 Giờ Sáng, Khách Hàng Báo Website Chết — Tôi Phải SSH Vào 5 VPS Để Tìm Lỗi
Tháng 11/2024, tôi nhận được tin nhắn Telegram lúc 3:15 sáng từ một khách hàng: "Website em không vào được, báo 502". Tôi mở mắt, mò laptop. SSH vào VPS số 1 — chạy NGINX reverse proxy. tail -f /var/log/nginx/error.log — upstream connection refused. OK, problem là ở backend. Tôi SSH vào VPS số 2 — Node.js API. journalctl -u myapp -n 100 --no-pager — không có lỗi. Lạ.
Tôi SSH vào VPS số 3 — PostgreSQL database. tail -f /var/log/postgresql/postgresql-16-main.log — FATAL: remaining connection slots are reserved. Database hết connection pool. Nhưng tại sao? Tôi lại SSH vào VPS số 4 — một cron job chạy mỗi đêm. grep "ERROR" /var/log/cron.log — cron job bị loop, spawn 200 connection đến PostgreSQL trong 30 giây. Tổng cộng: 4 lần SSH, 4 lệnh log khác nhau, 45 phút debug. Tất cả vì tôi không có hệ thống log tập trung.
Sáng hôm sau, tôi cài Loki + Grafana. Một tuần sau, cùng một vấn đề xảy ra. Lần này tôi mở Grafana dashboard trên điện thoại, gõ {job="cron"} |= "ERROR" vào Loki query, thấy ngay cron job spawn connection ồ ạt trong 2 giây. Tổng thời gian debug: 3 phút, từ điện thoại, khi vẫn đang nằm trên giường.
Bài này là toàn bộ stack Loki + Promtail + Grafana tôi đang chạy trên 5 VPS: cài đặt, cấu hình label, alert, retention policy, và những cái bẫy tiêu tốn ổ cứng nhanh hơn bạn tưởng.
2. Loki Là Gì? Tại Sao Không Dùng ELK?
Loki là hệ thống aggregate log do Grafana Labs phát triển, thiết kế để hoạt động như "Prometheus cho log": nhẹ, dùng label để index (thay vì full-text index như Elasticsearch), và tích hợp hoàn hảo với Grafana. Khác biệt cốt lõi: Loki chỉ index metadata (labels như job, host, filename, app) và nén raw log thành chunk trên object storage. Elasticsearch index toàn bộ nội dung log — mạnh hơn về full-text search nhưng ngốn RAM và disk gấp 5-10 lần.
| Tiêu chí | Loki + Grafana | ELK (Elasticsearch + Logstash + Kibana) |
|---|---|---|
| Phương pháp index | Labels only (metadata) | Full-text inverted index |
| RAM cho 10GB log/ngày | ~500MB | ~4-8GB |
| Disk cho 1 tháng log | ~30GB (compressed) | ~150GB (indexed + compressed) |
| Search speed | Nhanh với label filter, chậm hơn với full-text grep | Nhanh với mọi loại search |
| Cài đặt | 3 binary (Loki, Promtail, Grafana) | 3 service (ES, Logstash, Kibana) + Beats |
| JVM overhead | Không (Go binary) | Có (Elasticsearch cần heap 4-32GB) |
| LogQL query language | Giống PromQL, học 5 phút | Lucene query syntax, DSL phức tạp |
| Tích hợp Prometheus | Native — cùng Grafana dashboard | Cần plugin hoặc giải pháp riêng |
| Alert từ log | Có — Loki Ruler + Alertmanager | Có — Watcher + Alerting |
| Multi-tenancy | Có — built-in | Có — phức tạp hơn |
| Object storage backend | S3, GCS, Azure Blob, filesystem | Filesystem (chính), S3 (snapshot) |
| Chi phí VPS tối thiểu | 2GB RAM (cho 5 server) | 8GB RAM (cho 5 server) |
Với hệ thống dưới 10 server, Loki là lựa chọn hiển nhiên. ELK chỉ xứng đáng khi bạn cần full-text search siêu nhanh hoặc đã có sẵn Elasticsearch cho mục đích khác (APM, search engine).
3. Cài Đặt Loki + Promtail + Grafana Trên VPS Bằng Docker Compose
3.1. Cấu trúc thư mục
/opt/loki/
├── docker-compose.yml
├── loki-config.yml
├── promtail-config.yml
├── grafana/
│ └── provisioning/
│ ├── datasources/
│ │ └── loki.yaml
│ └── dashboards/
│ └── loki-logs.json
├── loki-data/ # Persistent storage cho Loki
└── grafana-data/ # Persistent storage cho Grafana
3.2. Docker Compose
# docker-compose.yml
version: "3.8"
services:
loki:
image: grafana/loki:3.4
container_name: loki
restart: unless-stopped
user: "1000:1000"
command: -config.file=/etc/loki/loki-config.yml
ports:
- "3100:3100" # Loki API
volumes:
- ./loki-config.yml:/etc/loki/loki-config.yml:ro
- ./loki-data:/loki
networks:
- monitoring-net
promtail:
image: grafana/promtail:3.4
container_name: promtail
restart: unless-stopped
command: -config.file=/etc/promtail/promtail-config.yml
volumes:
- ./promtail-config.yml:/etc/promtail/promtail-config.yml:ro
- /var/log:/var/log:ro # System logs
- /var/lib/docker/containers:/var/lib/docker/containers:ro # Docker logs
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- monitoring-net
grafana:
image: grafana/grafana:11.6
container_name: grafana
restart: unless-stopped
user: "472:472"
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=CHANGE_ME_NOW
- GF_INSTALL_PLUGINS=grafana-lokiexplore-app
volumes:
- ./grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- monitoring-net
networks:
monitoring-net:
name: monitoring-net
3.3. Cấu hình Loki — loki-config.yml
# loki-config.yml
auth_enabled: false
server:
http_listen_port: 3100
grpc_listen_port: 9096
log_level: info
# Distributor — nhận log từ Promtail
distributor:
ring:
kvstore:
store: inmemory
# Ingester — buffer log trước khi flush ra storage
ingester:
chunk_idle_period: 30m # Flush chunk sau 30 phút không có log mới
max_chunk_age: 2h # Flush chunk sau tối đa 2 giờ
chunk_target_size: 1536000 # 1.5MB per chunk
chunk_retain_period: 30s
max_transfer_retries: 0
wal:
dir: /loki/wal
# Schema config — định nghĩa cách index log
schema_config:
configs:
- from: 2026-01-01
store: tsdb # TSDB format mới (nhanh hơn boltdb-shipper)
object_store: filesystem
schema: v13
index:
prefix: loki_index_
period: 24h # Mỗi 24h tạo một index mới
# Storage — nơi lưu chunks và index
storage_config:
tsdb_shipper:
active_index_directory: /loki/tsdb-index
cache_location: /loki/tsdb-cache
filesystem:
directory: /loki/chunks
# Retention — xóa log cũ để tiết kiệm disk
limits_config:
retention_period: 30d # Giữ log 30 ngày
max_entries_limit_per_query: 5000
max_global_streams_per_user: 10000
ingestion_rate_mb: 16
ingestion_burst_size_mb: 32
reject_old_samples: true
reject_old_samples_max_age: 168h # Từ chối log cũ hơn 7 ngày
# Compactor — gộp chunk nhỏ để tiết kiệm storage
compactor:
working_directory: /loki/compactor
compaction_interval: 10m
retention_enabled: true
delete_request_store: filesystem
# Ruler — alert từ log
ruler:
alertmanager_url: http://alertmanager:9093
ring:
kvstore:
store: inmemory
rule_path: /loki/rules
storage:
type: local
local:
directory: /loki/rules
⚠️ Bẫy #1: retention_enabled nhưng không set retention_period
Nếu bạn bật compactor.retention_enabled: true nhưng KHÔNG set limits_config.retention_period, mặc định retention là 0 (giữ vĩnh viễn). Đĩa của bạn sẽ đầy trong vài ngày nếu có nhiều log. Tôi từng để Loki chạy 2 tuần không set retention, nó ăn hết 48GB disk trên VPS 50GB. Luôn set retention_period và theo dõi disk usage.
3.4. Cấu hình Promtail — promtail-config.yml
# promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml # Ghi nhớ vị trí đọc file log
clients:
- url: http://loki:3100/loki/api/v1/push # Push log đến Loki
batchsize: 1048576 # Batch 1MB trước khi gửi
batchwait: 1s
timeout: 10s
scrape_configs:
# System logs — journald
- job_name: system
journal:
json: false
max_age: 12h
labels:
job: system
host: vps-01 # Label phân biệt server
relabel_configs:
- source_labels: ['__journal__systemd_unit']
target_label: 'unit'
- source_labels: ['__journal__hostname']
target_label: 'hostname'
# File logs — nginx, custom app
- job_name: app-logs
static_configs:
- targets:
- localhost
labels:
job: app
host: vps-01
__path__: /var/log/nginx/*.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']
target_label: 'container'
- source_labels: ['__meta_docker_container_id']
target_label: 'container_id'
- action: labelmap
regex: __meta_docker_container_label_(.+)
3.5. Khởi động stack
# Tạo thư mục data với đúng permission
mkdir -p loki-data grafana-data
chown -R 1000:1000 loki-data
chown -R 472:472 grafana-data
# Khởi động
docker compose up -d
# Kiểm tra
curl http://localhost:3100/ready # Loki
curl http://localhost:9080/ready # Promtail
curl http://localhost:3000/api/health # Grafana
# Mở Grafana: http://your-vps-ip:3000
# Login: admin / CHANGE_ME_NOW
4. Sử Dụng LogQL — Ngôn Ngữ Query Log Của Loki
LogQL là ngôn ngữ query của Loki, thiết kế giống PromQL. Nếu bạn đã quen với Prometheus, học LogQL mất 5 phút. Cấu trúc cơ bản:
# Cấu trúc LogQL
{stream_selector} |= "log filter"
# Ví dụ cơ bản
{job="app", host="vps-01"} # Tất cả log từ app trên vps-01
{job="docker", container="nginx"} # Tất cả log từ container nginx
{job="system"} |= "Out of memory" # Chỉ log chứa "Out of memory"
{job="system"} != "debug" # Loại trừ log chứa "debug"
# Regex filter
{job="nginx"} |~ "status=(4[0-9]{2}|5[0-9]{2})" # HTTP 4xx hoặc 5xx
{job="app"} |~ `(?i)error|fail|fatal` # Case-insensitive error
# Pipeline — extract label từ log
{job="nginx"}
| pattern `<ip> - - <_> "<method> <uri> <_>" <status> <size>`
| status >= 400
# Aggregate — đếm, tổng hợp
rate({job="nginx"}[1m]) # Log rate per second
sum by (container) (rate({job="docker"}[5m])) # Log rate theo container
count_over_time({job="app"} |= "ERROR" [1h]) # Số lỗi trong 1 giờ
topk(5, sum by (host) (rate({job="system"}[5m])) > 1) # Top 5 server có log rate cao nhất
💡 Tip: Trong Grafana Explore (Explore → Loki), bạn có thể bật "Explain" để xem query execution plan — Loki sẽ hiển thị từng bước xử lý query, thời gian thực hiện, số dòng log quét qua. Rất hữu ích để debug query chậm.
5. Cấu Hình Multi-Server: Push Log Từ 5 VPS Về 1 Loki
Mô hình production của tôi: 1 VPS chạy Loki + Grafana (VPS monitoring), và 4 VPS chạy Promtail gửi log về. Promtail chạy như systemd service trên mỗi server, không cần Docker.
5.1. Cài Promtail trên VPS không Docker
# Tải Promtail binary
cd /opt
curl -LO https://github.com/grafana/loki/releases/download/v3.4.0/promtail-linux-amd64.zip
unzip promtail-linux-amd64.zip
chmod +x promtail-linux-amd64
mv promtail-linux-amd64 /usr/local/bin/promtail
# Tạo config
mkdir -p /etc/promtail
# /etc/promtail/promtail-config.yml
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/lib/promtail/positions.yaml
clients:
- url: http://YOUR_LOKI_SERVER_IP:3100/loki/api/v1/push # Đổi IP thật của server Loki
# Nếu dùng WireGuard VPN nội bộ
# url: http://10.0.0.1:3100/loki/api/v1/push
batchsize: 1048576
batchwait: 1s
tls_config:
insecure_skip_verify: true # Nếu không có HTTPS (mạng nội bộ)
scrape_configs:
- job_name: system
journal:
max_age: 12h
labels:
job: system
host: vps-02 # ĐỔI hostname cho từng server
dc: sg # Label datacenter
- job_name: web-logs
static_configs:
- targets: [localhost]
labels:
job: web
host: vps-02
__path__: /var/log/nginx/access.log
- job_name: app-logs
static_configs:
- targets: [localhost]
labels:
job: backend
host: vps-02
__path__: /opt/myapp/logs/*.log
# Tạo systemd service
# /etc/systemd/system/promtail.service
[Unit]
Description=Promtail Log Agent
After=network.target
[Service]
Type=simple
User=root
ExecStart=/usr/local/bin/promtail -config.file=/etc/promtail/promtail-config.yml
Restart=always
RestartSec=10
LimitNOFILE=65536
[Install]
WantedBy=multi-user.target
# Khởi động
systemctl daemon-reload
systemctl enable --now promtail
systemctl status promtail
5.2. Sử dụng WireGuard VPN cho log traffic nội bộ
KHÔNG nên để port 3100 mở ra internet. Hoặc bạn dùng reverse proxy với basic auth, hoặc tốt hơn: dùng WireGuard VPN nội bộ. Tôi viết chi tiết về cách dựng WireGuard VPN riêng — chỉ cần 4 dòng lệnh, mỗi server thêm 1 peer config.
⚠️ Bẫy #2: Promtail gửi log qua internet không encryption
Promtail gửi log qua gRPC, mặc định không mã hóa. Nếu Loki server của bạn ở public IP và Promtail client gửi qua internet (không VPN), attacker có thể sniff được toàn bộ log — bao gồm API keys, tokens, passwords nếu app của bạn log nhạy cảm. Giải pháp: (1) Dùng WireGuard VPN nội bộ cho tất cả traffic Promtail → Loki; hoặc (2) Đặt NGINX/Traefik làm reverse proxy với TLS termination trước Loki; hoặc (3) Dùng tls_config với mTLS trong cả Promtail và Loki.
6. Alert Từ Log — Phát Hiện Lỗi Trước Khi Khách Hàng Biết
Tôi dùng Loki Ruler để tạo alert rule từ log. Ví dụ: alert khi NGINX có hơn 10 lỗi 500 trong 5 phút, hoặc khi container bị restart liên tục.
# /loki/rules/fake/nginx-alerts.yml (fake là tenant)
groups:
- name: nginx-alerts
rules:
- alert: HighHTTP5xxRate
expr: |
sum by (host) (
rate({job="web"} |~ "HTTP/[0-9.]+" 5[0-9]{2} [0-9]+ [0-9.]+ [0-9.]+" [5m])
) > 10
for: 5m
labels:
severity: critical
team: backend
annotations:
summary: "{{ $labels.host }}: HTTP 5xx rate cao"
description: "Server {{ $labels.host }} có {{ $value }} lỗi 5xx/giây trong 5 phút. Kiểm tra ngay."
- alert: ContainerCrashLoop
expr: |
rate({job="docker"} |~ "error|fatal|panic|killed" [10m]) > 5
for: 10m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.container }} có dấu hiệu crash loop"
- alert: DiskWillFull
expr: |
count_over_time({job="system", unit="sshd.service"} |= "disk usage" [1h]) > 0
for: 5m
labels:
severity: info
Cấu hình Alertmanager để gửi alert qua Telegram/Discord:
# Thêm Alertmanager vào docker-compose.yml
alertmanager:
image: prom/alertmanager:v0.28
container_name: alertmanager
restart: unless-stopped
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
ports:
- "9093:9093"
networks:
- monitoring-net
# alertmanager.yml
route:
receiver: 'telegram'
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: 'telegram'
telegram_configs:
- bot_token: 'YOUR_BOT_TOKEN'
chat_id: YOUR_CHAT_ID
message: |
🚨 {{ .GroupLabels.alertname }}
{{ range .Alerts }}
• {{ .Annotations.summary }}
{{ end }}
7. Tối Ưu Hiệu Năng & Chi Phí Storage
7.1. Retention policy thực tế
Sau khi chạy 6 tháng với 5 VPS (~2-5GB log/ngày), đây là số liệu disk usage thực tế của tôi:
| Retention | Disk usage | Phù hợp cho |
|---|---|---|
| 7 ngày | ~5-8GB | Dev/test, VPS cá nhân |
| 30 ngày | ~20-35GB | Production nhỏ, 3-5 server |
| 90 ngày | ~60-100GB | Production, có compliance requirement |
| 365 ngày | ~250-400GB | Cần object storage (S3), không nên dùng local disk |
⚠️ Bẫy #3: Quên giới hạn stream label cardinality
Mỗi unique combination của labels tạo ra một "stream" trong Loki. Nếu bạn dùng label có cardinality cao như container_id hoặc request_id, Loki sẽ tạo hàng nghìn stream mới mỗi phút, ngốn RAM index và làm chậm query. Quy tắc: chỉ dùng label có dưới 1000 giá trị unique. Dữ liệu có cardinality cao (IP, user ID, request ID) nên để trong log body, không phải label.
7.2. Query optimization
Một query Loki có thể quét hàng GB log nếu bạn không dùng stream selector đúng:
# ❌ Chậm — quét TẤT CẢ log
{job=~".+"} |= "error"
# ✅ Nhanh — lọc theo stream label trước, rồi mới grep body
{job="nginx", host="vps-01"} |= "error"
# ❌ Chậm — regex trên label cardinality cao
{job="app"} |~ `(?i)error|fail`
# ✅ Nhanh — dùng |= (contains) thay vì |~ (regex) khi có thể
{job="app"} |= "error" # LogQL tối ưu contains hơn regex
8. Grafana Dashboard Cho Loki — Từ Cơ Bản Đến Nâng Cao
Grafana có Loki data source built-in từ v8.0. Một dashboard log đầy đủ nên có:
- Log volume theo thời gian — histogram hiển thị log rate (log/giây) trong 24h qua. Phát hiện bất thường ngay lập tức (log spike = có vấn đề).
- Top error sources — Bảng liệt kê service/host có nhiều lỗi nhất, sắp xếp giảm dần.
- Live tail — Stream log real-time như
tail -fnhưng trên browser. - Log pattern analysis — Tự động nhóm log theo pattern giống nhau, phát hiện log spam.
- Alert status panel — Hiển thị alert đang firing từ Loki Ruler.
Bạn có thể import dashboard community ID 13639 (Loki Dashboard for NGINX) hoặc 15141 (Loki NGINX Service Mesh) từ Grafana.com.
# Provision Grafana dashboard tự động
# grafana/provisioning/datasources/loki.yaml
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:3100
isDefault: true
editable: false
jsonData:
maxLines: 1000
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: false
9. So Sánh Chi Phí: Loki vs ELK vs SaaS (Datadog, Grafana Cloud)
Cho hệ thống 5 VPS, ~3GB log/ngày:
| Giải pháp | Chi phí/tháng | RAM cần | Tự quản |
|---|---|---|---|
| Loki self-host (VPS 2GB) | ~100K (VPS 2GB TrumVPS) | ~500MB | Có |
| ELK self-host (VPS 8GB) | ~400K (VPS 8GB) | ~4-6GB | Có |
| Grafana Cloud Logs | Free (50GB log, 14 ngày retention) | 0 | Không |
| Datadog Log Management | ~$15-45 (tùy volume) | 0 | Không |
| AWS CloudWatch Logs | ~$5-10 (3GB/ngày, 30d) | 0 | Không |
Grafana Cloud có tier miễn phí 50GB log — đủ cho 2-3 VPS cá nhân. Nếu bạn mới bắt đầu và không muốn tự host, đây là lựa chọn tốt nhất. Khi nào vượt 50GB, bạn có thể migrate về self-host Loki.
10. Checklist Triển Khai Production
- ✅ Retention policy rõ ràng: 30 ngày cho production, 7 ngày cho staging
- ✅ Log qua VPN nội bộ: Không gửi raw log qua internet không encrypt
- ✅ Label strategy: Dùng label có cardinality thấp (job, host, dc, env)
- ✅ Alert rule: Ít nhất 3 alert — high error rate, container crash, disk warning
- ✅ Grafana admin password mạnh: Đổi từ mặc định, dùng OAuth nếu có
- ✅ Backup Loki data: Hoặc chấp nhận mất log history khi rebuild (log là ephemeral data)
- ✅ Monitor Loki itself: Loki expose metrics ở
:3100/metrics, scrape bằng Prometheus - ✅ Disk usage alert: Alert khi disk còn <20% trên VPS chạy Loki
- ✅ Rate limit: Set
ingestion_rate_mbđể tránh 1 Promtail client gửi quá nhiều log làm sập Loki - ✅ Log rotation ở source: Đừng để log file gốc (nginx, app) không có rotation — disk đầy thì Promtail cũng chết theo
💡 Mẹo: Log what matters, not everything
Rất dễ rơi vào bẫy "log tất cả để sau này cần". Nhưng log càng nhiều thì: (1) Disk càng nhanh đầy; (2) Query càng chậm; (3) Chi phí càng cao. Sau 1 tháng dùng Loki, tôi loại bỏ 40% log không cần thiết — debug log của thư viện, healthcheck request, static asset access log. Chỉ giữ: error log, access log (chỉ 4xx/5xx), application audit log, và system log quan trọng (OOM, disk, SSH login).
11. Tổng Kết
6 tháng sau đêm 3 giờ sáng đó, tôi không còn SSH vào server để đọc log nữa. Mỗi sáng mở Grafana dashboard, nhìn log volume chart — nếu mọi thứ phẳng lặng, tôi biết hệ thống khỏe. Nếu có spike, tôi click vào spike, xem log pattern, tìm nguyên nhân trong 30 giây.
Loki không phải là công cụ hoàn hảo — full-text search chậm hơn Elasticsearch, query syntax đôi khi gây khó chịu, và community dashboard còn ít hơn ELK. Nhưng với mức giá gần như miễn phí (chạy trên VPS 2GB) và thời gian setup dưới 1 giờ, nó là giải pháp log tập trung tốt nhất cho cá nhân và team nhỏ.
Nếu bạn đang quản lý nhiều hơn 2 VPS và vẫn SSH vào từng server để tail -f, đây là dấu hiệu bạn cần Loki. Thuê một VPS 2GB, dành 1 giờ cài Loki theo bài này, và tận hưởng cảm giác debug từ điện thoại khi vẫn nằm trên giường như tôi.
🚀 Cần VPS để chạy Loki + Grafana?
TrumVPS — VPS giá rẻ từ 47K/tháng, 2GB RAM đủ chạy Loki cho 5-10 server
Thuê VPS Ngay →