Grafana 完整教學 2026:Prometheus+Loki 自架監控堆疊

用一份 docker-compose 把 Grafana 13.3、Prometheus 3.15、Loki 3.7 與 Alloy 一次架起來:五個容器實測記憶體共約 645MB,附完整設定檔、PromQL/LogQL 起手式,以及實測踩到的五個坑(Loki 400 timestamp too old、compose 埠號覆蓋要用 !override、node-exporter 要 pid: host)。

  • Dennis
  • 11 分鐘閱讀
Grafana 完整教學 2026:Prometheus+Loki 自架監控堆疊

Grafana 13.3+Prometheus 3.15+Loki 3.7+Alloy 1.21 用一份 docker-compose 就能架完,五個容器實測記憶體約 645MB,能同時看指標、查日誌、發告警。

為什麼已經有 Uptime Kuma 還需要這一套

如果你照著站上Uptime Kuma 完整教學架了監控,你已經知道「服務掛了會叫」。但你一定有過這種時刻:手機跳出「網站 503」,你打開終端機、翻 docker logs、猜是哪個容器、猜是不是記憶體爆掉。

Uptime Kuma 回答的是「活著嗎」;Grafana 這套回答的是「為什麼」。兩者不是替代關係:

你有的工具它回答的問題對應元件
Uptime Kuma服務活著嗎?憑證過期沒?黑箱探測(HTTP/TCP/Ping)
PrometheusCPU、記憶體、磁碟、連線數現在多少?趨勢往哪走?白箱指標(metrics)
Loki出事那三分鐘,log 裡到底寫了什麼?日誌聚合(logs)
Grafana把上面三個放在同一張圖上,並且在超標時叫人儀表板+告警

換句話說,Uptime Kuma 是門口的警衛,Grafana 堆疊是整棟樓的監視器加電表。這篇就是把後者架起來,而且不用按任何一次滑鼠——所有資料來源都用檔案宣告,重建環境不用重來一遍。

一表看懂:四個角色各做什麼

元件版本(2026-10-29 查證)授權星數角色實測記憶體
Grafana13.3.0AGPL-3.0約 77.6K儀表板、告警、資料來源代理約 494 MB
Prometheus3.15.1Apache-2.0約 66.9K拉取並儲存時序指標約 37 MB
Loki3.7.9AGPL-3.0約 29.1K日誌聚合(「日誌版的 Prometheus」)約 56 MB
Alloy1.21.3Apache-2.0約 3.7K採集代理,把日誌送進 Loki約 51 MB
node-exporter1.9.1Apache-2.0—把主機 CPU/記憶體/磁碟變成指標約 7 MB

加起來大約 645 MB 記憶體,這是在一台 7.7GB RAM 的 VPS 上實測的數字。硬碟則看你保留多久的日誌與指標:Prometheus 預設保留 30 天、Loki 這份設定保留 31 天(744 小時)。

資料是怎麼流的

不用畫圖也看得懂,就三條線:

方向起點經過終點
指標線node-exporter(主機)Prometheus 每 15 秒來拉一次Prometheus 時序資料庫
日誌線各容器的 stdoutAlloy 讀 Docker socketLoki
呈現線Prometheus+LokiGrafana 當資料來源查詢你的瀏覽器

關鍵差別是「推」與「拉」:Prometheus 是主動去拉(pull),所以你的服務只要有 /metrics 端點就會被動被監控;Loki 則是被動接收(push),所以需要 Alloy 這種代理去把日誌推過去。理解這點,後面所有設定就都不難了。

安裝:目錄與三份設定檔

先在要放監控的機器上開一個目錄:

mkdir -p ~/observability/grafana/provisioning/datasources
cd ~/observability

總共會用四個檔案:docker-compose.yaml、prometheus.yml、loki-config.yaml、config.alloy,外加一個 Grafana 的資料來源宣告檔。

1. docker-compose.yaml

services:
  grafana:
    image: grafana/grafana:13.3.0
    container_name: obs-grafana
    restart: unless-stopped
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=changeme
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
    depends_on:
      - prometheus
      - loki

  prometheus:
    image: prom/prometheus:v3.15.1
    container_name: obs-prometheus
    restart: unless-stopped
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.path=/prometheus
      - --storage.tsdb.retention.time=30d
      - --web.enable-lifecycle
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus

  loki:
    image: grafana/loki:3.7.9
    container_name: obs-loki
    restart: unless-stopped
    command: -config.file=/etc/loki/config.yaml
    ports:
      - "3100:3100"
    volumes:
      - ./loki-config.yaml:/etc/loki/config.yaml:ro
      - loki-data:/loki

  alloy:
    image: grafana/alloy:v1.21.3
    container_name: obs-alloy
    restart: unless-stopped
    command:
      - run
      - /etc/alloy/config.alloy
      - --storage.path=/var/lib/alloy/data
      - --server.http.listen-addr=0.0.0.0:12345
    ports:
      - "12345:12345"
    volumes:
      - ./config.alloy:/etc/alloy/config.alloy:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - alloy-data:/var/lib/alloy/data
    depends_on:
      - loki

  node-exporter:
    image: prom/node-exporter:v1.9.1
    container_name: obs-node-exporter
    restart: unless-stopped
    command:
      - --path.rootfs=/host
    pid: host
    volumes:
      - /:/host:ro,rslave
    ports:
      - "9100:9100"

volumes:
  grafana-data:
  prometheus-data:
  loki-data:
  alloy-data:

三個地方特別容易寫錯,先標出來:

  • pid: host 不能省。node-exporter 要讀得到主機的 /proc 才能算出真實的 CPU 使用率;少了它,你會在圖表上看到容器自己的視角,數字全部失真。
  • Docker socket 一定要掛 :ro。Alloy 只需要「讀」日誌,給寫入權限等於把整台主機的 root 交給一個容器。
  • 映像檔版本我全部釘死(不是 :latest)。監控系統最怕的就是「重開機之後 Grafana 升級、面板全跑版」。

2. prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s
  external_labels:
    monitor: homelab

scrape_configs:
  - job_name: prometheus
    static_configs:
      - targets: ["localhost:9090"]

  - job_name: node
    static_configs:
      - targets: ["node-exporter:9100"]

srape_interval: 15s 是很實用的折衷:再短(例如 5s)會讓時序資料量暴增三倍,再長則告警會慢半拍。如果你的機器很吃緊(1GB RAM 以下),改成 60s 也完全夠用。

設定檔寫完,可以先用官方工具驗語法,不要等到容器起不來才猜:

docker run --rm \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml:ro \
  --entrypoint promtool prom/prometheus:v3.15.1 \
  check config /etc/prometheus/prometheus.yml

實測輸出:

Checking /etc/prometheus/prometheus.yml
 SUCCESS: /etc/prometheus/prometheus.yml is valid prometheus config file syntax

3. loki-config.yaml

這是整篇最容易踩雷的檔案。官方 getting-started 範例是給「分散式部署」用的(讀寫分離+MinIO+nginx 閘道,共七個容器),照抄到小主機上會很痛苦。單機版用下面這份:

auth_enabled: false

server:
  http_listen_port: 3100
  grpc_listen_port: 9096
  log_level: info

common:
  instance_addr: 127.0.0.1
  path_prefix: /loki
  storage:
    filesystem:
      chunks_directory: /loki/chunks
      rules_directory: /loki/rules
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: 2024-01-01
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

limits_config:
  retention_period: 744h
  allow_structured_metadata: true
  reject_old_samples: false

compactor:
  working_directory: /loki/compactor
  retention_enabled: true
  delete_request_store: filesystem

ruler:
  storage:
    type: local
    local:
      directory: /loki/rules

四個關鍵字說明:

  • retention_period: 744h 是 31 天。光寫這行沒有用,還要加上 compactor 區塊的 retention_enabled: true 與 delete_request_store: filesystem,否則日誌只會一直長大、不會被刪。
  • reject_old_samples: false 是本文實測出來最重要的一行,原因見下面「五個坑」的第一條。
  • allow_structured_metadata: true 讓 Loki 自動辨識日誌等級(打開後查詢結果會多一個 detected_level 標籤,可以直接篩 error)。
  • auth_enabled: false 是單租戶模式。只有自己用沒問題,但不要把 3100 埠直接曝露到公網,Loki 在這模式下沒有驗證。

4. config.alloy

Alloy 是 Grafana 近年用來取代 Promtail/Grafana Agent 的新代理,設定語言是 HCL 風格的 .alloy:

discovery.docker "containers" {
  host             = "unix:///var/run/docker.sock"
  refresh_interval = "5s"
}

discovery.relabel "containers" {
  targets = []

  rule {
    source_labels = ["__meta_docker_container_name"]
    regex         = "/(.*)"
    target_label  = "container"
  }

  rule {
    source_labels = ["__meta_docker_container_log_stream"]
    target_label  = "stream"
  }

  rule {
    source_labels = ["__meta_docker_container_label_com_docker_compose_service"]
    target_label  = "service"
  }
}

loki.source.docker "containers" {
  host             = "unix:///var/run/docker.sock"
  targets          = discovery.docker.containers.targets
  forward_to       = [loki.write.local.receiver]
  relabel_rules    = discovery.relabel.containers.rules
  refresh_interval = "5s"
}

loki.write "local" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

那段 regex = "/(.*)" 不是裝飾:Docker 回報的容器名稱長得像 /obs-grafana,前面有一個斜線,不去掉的話你的標籤會變成 container="/obs-grafana",之後查詢怎麼寫都對不上。

5. Grafana 資料來源自動掛載

grafana/provisioning/datasources/ds.yaml:

apiVersion: 1

datasources:
  - name: Prometheus
    type: prometheus
    uid: prometheus
    access: proxy
    url: http://prometheus:9090
    isDefault: true
    jsonData:
      timeInterval: 15s

  - name: Loki
    type: loki
    uid: loki
    access: proxy
    url: http://loki:3100
    jsonData:
      maxLines: 1000

這段是「可重現」的關鍵。手動點 UI 加資料來源也可以,但下次換機器你就得再點一次、還可能點錯;用檔案宣告,git clone 完 docker compose up -d 就是一台設定好的監控主機。

啟動與驗證:每一項都要看到 200

docker compose config --quiet && echo "設定檔語法 OK"
docker compose up -d

等大約 30 秒再驗證。實測輸出如下:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000/api/health   # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9090/-/ready     # 200
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3100/ready       # ready
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:9100/metrics     # 200

再確認 Prometheus 真的有抓到兩個目標(1 代表健康):

curl -s 'http://127.0.0.1:9090/api/v1/query?query=up'

實測回傳:

{"status":"success","data":{"resultType":"vector","result":[
 {"metric":{"__name__":"up","instance":"localhost:9090","job":"prometheus"},"value":[1793304221.051,"1"]},
 {"metric":{"__name__":"up","instance":"node-exporter:9100","job":"node"},"value":[1793304221.051,"1"]}]}}

Grafana 的資料來源也可以直接用 API 驗(不需要開瀏覽器):

curl -s -u admin:changeme \
  'http://127.0.0.1:3000/api/datasources/proxy/uid/prometheus/api/v1/query?query=up'

回傳跟上面一樣的 JSON,就代表「Grafana → Prometheus」這一段通了。這個指令很好用,出問題時可以快速二分法:是資料源壞了,還是面板壞了。

實測踩到的五個坑

這五條都是我實際把這套跑起來時遇到的,不是抄文件:

坑 1:Loki 一直回 400,日誌全部被丟掉

Alloy 第一次啟動時會把 Docker 裡既有的歷史日誌整批讀進來,其中很多是幾個月前的。Loki 的 reject_old_samples 預設會拒絕太舊的樣本,於是 Alloy 不斷重試、不斷收到 400:

level=error msg="final error sending batch, no retries left, dropping data"
  status=400 error="... entry has timestamp too old: 2026-03-05T17:26:28Z,
  oldest acceptable timestamp is: 2026-10-22T20:02:51Z"

解法就是上面那份 loki-config.yaml 裡的 reject_old_samples: false。改完重建容器,實測 timestamp too old 錯誤數從滿畫面變成 0,Alloy 的 dropping data 也是 0。

如果你不想全關掉這個保護,替代做法是把 reject_old_samples_max_age 調長(例如 2160h)。但對「剛接上既有主機」的情境,先關掉再讓資料自然過期,實務上最省事。

坑 2:官方 getting-started 的 compose 檔不適合小主機

我照官方文件抓了 examples/getting-started/docker-compose.yaml(Loki v3.7.0),打開發現它是分散式架構:read、write、backend 三個 Loki 角色+minio 物件儲存+nginx 當閘道+flog 產生假日誌,一共七個容器。這在單機上完全是小題大作,而且 MinIO 社群版早已停止維護(站上自架 S3 那篇有詳細說明替代方案)。

結論:官方 Docker 安裝頁面同時提供「單容器 docker run」與「Compose」兩種,小主機請走單機設定,不要抄 getting-started 的 compose。

坑 3:想改埠號時,compose 的 override 是「相加」不是「取代」

我為了避開已被佔用的 3100 埠,寫了一個 docker-compose.test.yaml 想覆蓋成 3101:

services:
  loki:
    ports:
      - "3101:3100"

結果啟動直接失敗:

failed to bind host port 0.0.0.0:3100/tcp: address already in use

因為 Compose 合併 ports 是聯集,3100 與 3101 都留著。正確寫法要用 !override 標籤:

services:
  loki:
    ports: !override
      - "3101:3100"

(需要 Docker Compose v2.24 以上;本篇環境是 v5.1.0。)這個坑值得記住,因為它也會發生在你幫其他服務寫 dev/prod 兩份 compose 的時候。

坑 4:Grafana 13 的記憶體比你想的高

實測閒置狀態:Grafana 約 494 MB,比 Loki(56 MB)+Prometheus(37 MB)+Alloy(51 MB)加起來還多。這是 Grafana 13 拉了更多前端資源與外掛的結果。在 1GB RAM 的小機器上,這一顆就會把記憶體吃光——如果你的機器很迷你,考慮改用 grafana/grafana:13.3.0-slim(不含預載外掛),或把 Grafana 放在比較大的機器上、其餘元件留在小主機。

坑 5:Loki 剛啟動時 /ready 會回 503,不要以為壞了

Ingester not ready: waiting for 15s after being ready

這是 Loki 的正常行為(ingester 暖機),大約 15~30 秒後就變 ready。如果你用腳本自動部署,健康檢查的 start_period 要設 30 秒以上,不然會被誤判成失敗而重啟,然後永遠起不來。

PromQL 與 LogQL 起手式

裝好之後最常被問的是「那我現在要看什麼」。這幾條貼進 Grafana 的 Explore 就能用:

用途語法備註
主機 CPU 使用率(%)100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[1m])) * 100)實測回傳約 18%,與 top 相符
可用記憶體(%)100 * node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes比 MemFree 準,因為含快取
根目錄磁碟使用率100 - 100 * node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}小主機最常爆的一項
目標是否活著up == 0搭配告警用,最實用的一條
找某容器最近的錯誤{container="obs-grafana"} |= "error"LogQL,|= 是「包含」
跨容器找錯誤{service=~".+"} |~ "(?i)error|fail"正則比對,注意 Loki 預設只回最近 1,000 行

LogQL 這一段有個實測發現:開啟 allow_structured_metadata 後,查詢結果會自動多一個 detected_level 標籤,於是你可以直接寫 {detected_level="error"},不用再去猜每支程式把錯誤寫成 ERROR、error 還是 ERR。這是 Loki 3.x 相對舊版最省事的功能之一。

告警:不要只看儀表板,要會叫人

Grafana 內建的統一告警(Alerting)可以直接對 Prometheus 下條件,不用另外架 Alertmanager。最小可用的一條規則長這樣(在 UI 建,或寫成 provisioning 檔):

  • 查詢:up == 0
  • 條件:IS BELOW 1
  • 持續時間:For 2m(避免瞬斷誤報)
  • 通知:Telegram/Email/Slack contact point

建議一開始只設三條:up == 0(服務掛)、磁碟 > 90%、記憶體可用 < 10%。這三條能涵蓋自架環境八成的意外。站上Portainer 完整教學裡提到的容器重啟循環,用 up 這條也能抓到。

備份與資料保留

監控資料通常「掉了不痛」,但設定檔掉了很痛。備份策略分兩層:

對象要不要備份做法
docker-compose.yaml、prometheus.yml、loki-config.yaml、config.alloy、provisioning一定要丟進 git,這是整套系統真正的資產
Prometheus 資料(prometheus-data)通常不用30 天後自然輪替,除非要做長期趨勢分析
Grafana 資料庫(grafana-data)建議要面板 JSON 用 API 匯出更乾淨:GET /api/dashboards/uid/<uid>
Loki 日誌(loki-data)看用途合規需求才需要;一般除錯靠 31 天足夠

如果你已經有 restic 或 Kopia 在跑(自架備份三選一那篇有完整設定),把上面那幾個 YAML 目錄加進備份清單就好,不需要備份整個 volume。

已知限制:這套不是萬靈丹

照本站慣例,講清楚沒做的部分:

  • 本文只實測單機 Docker 部署,沒有測 Kubernetes、沒有測多節點 Loki 叢集。站上另有一篇 Prometheus 與 Kubernetes 監控談 K8s 情境,兩者不要混用同一份設定。
  • 沒有實作 tracing。這套是 metrics+logs,缺了 traces 那一塊(要 Loki 3.x 的衍生欄位或 Tempo 才能串起來)。對大部分自架服務,metrics+logs 已經夠用。
  • 沒有測高流量情境。實測的是五個容器、每天幾十 MB 日誌的家庭/小型團隊規模。日誌量到每天 GB 級時,Loki 的儲存與查詢就會需要調 chunk 大小與 compactor 參數,這篇沒有涵蓋。
  • 版本會動。文中版本號是 2026-10-29 以 GitHub Releases API 查證的當下最新版;Grafana 生態改版很快(例如 Alloy 取代 Promtail 就是近兩年的事),實際安裝前建議再確認一次映像檔標籤存在。
  • reject_old_samples: false 不是無腦推薦。它會讓 Loki 接受很舊的日誌,如果你有「日誌時間戳必須可信」的稽核需求,請改用調長 max_age 的做法。

誰適合、誰不需要

情境建議
只有一台 VPS、兩個網站先用 Uptime Kuma就好,這套對你可能過重
自架 5 個以上服務、常需要查「為什麼變慢」這套值得,投入約半天
有合規要求、日誌要留 30 天以上可查這套是標配
團隊多人,需要共用儀表板這套很適合,Grafana 權限與資料夾管理成熟
只想看有沒有掛掉不需要,Uptime Kuma 更輕

站內延伸閱讀

原始來源

利益揭露:本文無聯盟連結。文中版本號、星數與記憶體數字為 2026-10-29 在本站 VPS(7.7GB RAM、Docker 29.2.1、Compose v5.1.0)實測結果,你的環境可能不同。

📬 訂閱 most.tw 電子報

每週精選 AI 工具教學與技術乾貨,直接送到你的信箱。免費、隨時可退訂。

訂閱即表示同意收到 most.tw 電子報,隨時可一鍵退訂。

💬 有問題想討論?加 LINE 聯絡我

歡迎透過 LINE 官方帳號直接留言,我會盡快回覆你的問題。

加入 LINE 好友