Quản Lý Log VPS Với Loki + Grafana 2026 — Không Còn Mò Mẫm SSH Từng Server Để Đọc Log

📅 28/06/2026 · ⏱ 16 phút đọc · 🏷 DevOps · Monitoring · Log Management

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 + GrafanaELK (Elasticsearch + Logstash + Kibana)
Phương pháp indexLabels 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 speedNhanh với label filter, chậm hơn với full-text grepNhanh với mọi loại search
Cài đặt3 binary (Loki, Promtail, Grafana)3 service (ES, Logstash, Kibana) + Beats
JVM overheadKhông (Go binary)Có (Elasticsearch cần heap 4-32GB)
LogQL query languageGiống PromQL, học 5 phútLucene query syntax, DSL phức tạp
Tích hợp PrometheusNative — cùng Grafana dashboardCần plugin hoặc giải pháp riêng
Alert từ logCó — Loki Ruler + AlertmanagerCó — Watcher + Alerting
Multi-tenancyCó — built-inCó — phức tạp hơn
Object storage backendS3, GCS, Azure Blob, filesystemFilesystem (chính), S3 (snapshot)
Chi phí VPS tối thiểu2GB 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:

RetentionDisk usagePhù hợp cho
7 ngày~5-8GBDev/test, VPS cá nhân
30 ngày~20-35GBProduction nhỏ, 3-5 server
90 ngày~60-100GBProduction, có compliance requirement
365 ngày~250-400GBCầ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ó:

  1. 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 đề).
  2. 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.
  3. Live tail — Stream log real-time như tail -f nhưng trên browser.
  4. Log pattern analysis — Tự động nhóm log theo pattern giống nhau, phát hiện log spam.
  5. 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ápChi phí/thángRAM cầnTự quản
Loki self-host (VPS 2GB)~100K (VPS 2GB TrumVPS)~500MB
ELK self-host (VPS 8GB)~400K (VPS 8GB)~4-6GB
Grafana Cloud LogsFree (50GB log, 14 ngày retention)0Không
Datadog Log Management~$15-45 (tùy volume)0Không
AWS CloudWatch Logs~$5-10 (3GB/ngày, 30d)0Khô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

  1. ✅ Retention policy rõ ràng: 30 ngày cho production, 7 ngày cho staging
  2. ✅ Log qua VPN nội bộ: Không gửi raw log qua internet không encrypt
  3. ✅ Label strategy: Dùng label có cardinality thấp (job, host, dc, env)
  4. ✅ Alert rule: Ít nhất 3 alert — high error rate, container crash, disk warning
  5. ✅ Grafana admin password mạnh: Đổi từ mặc định, dùng OAuth nếu có
  6. ✅ Backup Loki data: Hoặc chấp nhận mất log history khi rebuild (log là ephemeral data)
  7. ✅ Monitor Loki itself: Loki expose metrics ở :3100/metrics, scrape bằng Prometheus
  8. ✅ Disk usage alert: Alert khi disk còn <20% trên VPS chạy Loki
  9. ✅ Rate limit: Set ingestion_rate_mb để tránh 1 Promtail client gửi quá nhiều log làm sập Loki
  10. ✅ 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 →