Aplikasi yang sudah berjalan stabil di production tetap butuh jawaban cepat atas tiga pertanyaan sehari-hari: seberapa sehat resource yang dipakai container saat ini, kenapa request tertentu tiba-tiba lambat, dan di baris log mana persisnya sebuah error terjadi. Mengandalkan docker stats atau docker logs satu per satu masih bisa dipakai untuk debugging cepat, tapi jelas tidak praktis begitu jumlah container bertambah jadi puluhan atau saat insiden butuh korelasi antara metrik dan log dalam rentang waktu yang sama. Bab ini membangun satu stack observabilitas lengkap berbasis Docker Compose, yaitu kombinasi tooling untuk mengamati kondisi sistem secara menyeluruh, memakai Prometheus untuk mengumpulkan metrik, Grafana untuk visualisasi dan dashboard, serta Loki untuk log terpusat, lalu menghubungkan ketiganya dengan aplikasi container yang sudah dibahas pada studi kasus-studi kasus sebelumnya.
34.1 Setup Prometheus untuk Metrics Collection
Prometheus adalah monitoring system dan time series database open-source yang mengumpulkan metrik dengan cara menarik data secara berkala dari target yang sudah didaftarkan, pola yang disebut pull-based scraping. Setiap target cukup menyediakan satu endpoint HTTP berisi metrik dalam format teks yang sudah distandarkan, dan Prometheus yang mengunjungi endpoint tersebut sesuai interval yang dikonfigurasi, kebalikan dari pola push-based yang mengharuskan aplikasi aktif mengirim data ke server monitoring.
34.1.1 Arsitektur Stack Observabilitas
Stack pada bab ini terdiri dari enam service yang saling melengkapi. Service prometheus bertugas menyimpan dan menyediakan metrik lewat query. Service cadvisor (singkatan dari Container Advisor) mengekspos metrik penggunaan CPU, memori, network, dan disk per container. Service node-exporter mengekspos metrik level host, seperti penggunaan CPU dan memori mesin secara keseluruhan. Service grafana menampilkan seluruh metrik tersebut dalam bentuk dashboard visual. Service loki menyimpan log terpusat dari seluruh container, sementara alloy bertugas mengumpulkan log dari tiap container dan mengirimkannya ke Loki. Sysadmin/DevOps Engineer biasanya menempatkan seluruh service ini dalam satu compose.yaml terpisah dari aplikasi utama, supaya stack observabilitas bisa dikelola dan di-upgrade independen dari siklus deployment aplikasi yang diawasinya.
Susun kerangka compose.yaml dengan service prometheus, cadvisor, dan node-exporter terlebih dahulu sebagai fondasi pengumpulan metrik.
services:
prometheus:
image: prom/prometheus:v3.13.3
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus-data:/prometheus
ports:
- "9090:9090"
networks:
- observability-net
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.56.2
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
networks:
- observability-net
node-exporter:
image: prom/node-exporter:v1.12.1
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- "--path.procfs=/host/proc"
- "--path.sysfs=/host/sys"
- "--path.rootfs=/rootfs"
networks:
- observability-net
networks:
observability-net:
volumes:
prometheus-data:Konfigurasi cadvisor dan node-exporter sama-sama meminjam sejumlah path dari filesystem host lewat bind mount mode ro (read-only), karena keduanya memang perlu membaca informasi dari kernel dan runtime Docker di level host untuk bisa mengukur pemakaian resource secara akurat. Mode read-only ini penting sebagai lapisan keamanan tambahan, supaya kedua container ini tidak bisa menulis apapun ke filesystem host meski punya akses baca yang luas.
34.1.2 Konfigurasi prometheus.yml dan Scrape Target
File prometheus.yml mendefinisikan target mana saja yang harus di-scrape Prometheus beserta intervalnya, sesuai format konfigurasi resmi Prometheus.
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "cadvisor"
static_configs:
- targets: ["cadvisor:8080"]
- job_name: "node-exporter"
static_configs:
- targets: ["node-exporter:9100"]
- job_name: "api"
metrics_path: /metrics
static_configs:
- targets: ["api:3000"]job_name di sini bebas dipakai sebagai label untuk membedakan asal metrik saat query, sementara targets memakai nama service Compose karena Prometheus berjalan dalam network internal yang sama dan bisa me-resolve nama service lewat DNS bawaan Docker, pola yang sama seperti komunikasi antar-container pada studi kasus sebelumnya. Job api pada contoh ini mengasumsikan aplikasi backend sudah mengekspos endpoint /metrics sendiri lewat instrumentasi library client Prometheus di sisi kode aplikasi.
Restart service prometheus setelah mengubah prometheus.yml, lalu buka antarmuka web Prometheus untuk memverifikasi status tiap target.
docker compose up -d prometheus
curl -s http://localhost:9090/-/healthyAkses http://localhost:9090/targets dari browser untuk melihat status UP atau DOWN tiap target yang didefinisikan. Target berstatus DOWN biasanya menandakan Prometheus belum bisa menjangkau endpoint tersebut, entah karena nama service salah ketik, port belum sesuai, atau container target belum sepenuhnya siap menerima koneksi.
34.2 Grafana untuk Visualisasi dan Dashboard
Grafana adalah platform visualisasi open-source yang membaca data dari berbagai data source, termasuk Prometheus dan Loki, lalu menampilkannya sebagai dashboard interaktif berisi grafik, tabel, dan panel lainnya. Tambahkan service grafana ke dalam compose.yaml yang sama.
34.2.1 Setup Grafana dan Koneksi Data Source
services:
grafana:
image: grafana/grafana:13.2.1
ports:
- "3001:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD}
volumes:
- grafana-data:/var/lib/grafana
- ./grafana/provisioning:/etc/grafana/provisioning:ro
networks:
- observability-netVariabel GF_SECURITY_ADMIN_PASSWORD menimpa password admin default Grafana, sesuai dokumentasi resmi konfigurasi Grafana lewat environment variable, dan sebaiknya diisi lewat file .env yang tidak ikut masuk ke version control, sama seperti pola pengelolaan kredensial pada studi kasus lain. Volume grafana-data menjaga dashboard, user, dan pengaturan lain tetap ada meski container Grafana dihapus dan dibuat ulang.
Daripada menambahkan data source Prometheus secara manual lewat antarmuka web setiap kali stack dibangun ulang, manfaatkan provisioning, yaitu mekanisme Grafana membaca konfigurasi data source dan dashboard dari file YAML saat startup. Buat grafana/provisioning/datasources/prometheus.yaml.
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://prometheus:9090
isDefault: trueBuat juga grafana/provisioning/datasources/loki.yaml untuk data source Loki yang dibahas pada bagian logging.
apiVersion: 1
datasources:
- name: Loki
type: loki
access: proxy
url: http://loki:310034.2.2 Provisioning Dashboard Otomatis
Grafana juga bisa memuat file dashboard JSON secara otomatis lewat provisioning, sehingga dashboard tidak hilang begitu volume Grafana direset. Buat grafana/provisioning/dashboards/default.yaml untuk mendaftarkan folder tempat file dashboard disimpan.
apiVersion: 1
providers:
- name: "default"
folder: ""
type: file
options:
path: /etc/grafana/provisioning/dashboards/jsonTaruh file dashboard JSON hasil export dari Grafana, atau dashboard komunitas yang diunduh dari Grafana Dashboards Marketplace (misalnya dashboard resmi cAdvisor atau Node Exporter Full), ke dalam folder grafana/provisioning/dashboards/json. Restart service grafana agar dashboard tersebut otomatis muncul.
docker compose up -d grafanaLogin ke Grafana lewat http://localhost:3001 memakai user admin dan password sesuai GRAFANA_ADMIN_PASSWORD, lalu periksa menu Connections > Data sources untuk memastikan Prometheus dan Loki sudah terdaftar dengan status koneksi yang sukses tanpa perlu konfigurasi manual apapun.
34.3 Loki untuk Centralized Logging
Loki adalah sistem log aggregation dari Grafana Labs yang dirancang khusus untuk efisien menyimpan log dalam jumlah besar, dengan pendekatan mengindeks label metadata saja alih-alih mengindeks seluruh isi teks log seperti pada Elasticsearch. Pendekatan ini membuat Loki jauh lebih ringan dari sisi resource, cocok dipakai Sysadmin/DevOps Engineer yang ingin sentralisasi log tanpa harus mengoperasikan cluster pencarian teks yang berat.
34.3.1 Setup Loki dan Grafana Alloy
Pengumpulan log dari container ke Loki dilakukan lewat Grafana Alloy, yaitu collector resmi Grafana Labs yang menggantikan Promtail sebagai agen pengumpul log. Promtail sudah dinyatakan deprecated sejak Loki versi 3.0 dan kodenya sepenuhnya digabung ke Alloy, sesuai catatan rilis resmi Loki, sehingga Alloy adalah pilihan yang tepat untuk stack yang dibangun saat ini. Tambahkan service loki dan alloy ke dalam compose.yaml.
services:
loki:
image: grafana/loki:3.7.7
ports:
- "3100:3100"
volumes:
- ./loki-config.yaml:/etc/loki/local-config.yaml:ro
- loki-data:/loki
networks:
- observability-net
alloy:
image: grafana/alloy:1.19.2
ports:
- "12345:12345"
volumes:
- ./config.alloy:/etc/alloy/config.alloy:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
command:
- run
- --server.http.listen-addr=0.0.0.0:12345
- --storage.path=/var/lib/alloy/data
- /etc/alloy/config.alloy
networks:
- observability-netAlloy perlu akses ke docker.sock supaya bisa membaca metadata container lewat Docker API, sekaligus menarik stream log tiap container langsung dari Docker daemon, tanpa harus membaca file JSON log mentah dari filesystem host seperti pendekatan lama. Port 12345 yang dipetakan ke host menyediakan antarmuka web debugging bawaan Alloy, tempat memantau status tiap komponen pipeline secara real-time.
Buat config.alloy berisi definisi pipeline pengumpulan log, ditulis dalam sintaks deklaratif berbasis komponen milik Alloy.
discovery.docker "containers" {
host = "unix:///var/run/docker.sock"
refresh_interval = "5s"
}
discovery.relabel "containers" {
targets = discovery.docker.containers.targets
rule {
source_labels = ["__meta_docker_container_name"]
regex = "/(.*)"
target_label = "container"
}
}
loki.source.docker "containers" {
host = "unix:///var/run/docker.sock"
targets = discovery.docker.containers.targets
relabel_rules = discovery.relabel.containers.rules
forward_to = [loki.process.containers.receiver]
}
loki.process "containers" {
stage.docker {}
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki:3100/loki/api/v1/push"
}
}Komponen discovery.docker mendeteksi seluruh container yang berjalan di host lewat Docker API, otomatis mengenali container baru begitu dijalankan tanpa perlu mendaftarkan target secara manual satu per satu. Komponen discovery.relabel mengambil nama container dari label metadata __meta_docker_container_name dan menjadikannya label container, memudahkan proses filter log berdasarkan nama container saat query di Grafana nanti. Komponen loki.source.docker menarik stream log dari tiap container hasil deteksi tersebut, lalu meneruskannya ke loki.process yang menerapkan stage.docker untuk mengurai format JSON log bawaan Docker menjadi baris teks log yang bersih. Komponen terakhir, loki.write, mengirim hasil akhirnya ke endpoint push Loki.
Buat juga loki-config.yaml dengan konfigurasi dasar penyimpanan lokal, cukup memadai untuk kebutuhan single-node.
auth_enabled: false
server:
http_listen_port: 3100
common:
path_prefix: /loki
storage:
filesystem:
chunks_directory: /loki/chunks
rules_directory: /loki/rules
replication_factor: 1
ring:
instance_addr: 127.0.0.1
kvstore:
store: inmemory
schema_config:
configs:
- from: 2024-01-01
store: tsdb
object_store: filesystem
schema: v13
index:
prefix: index_
period: 24h34.3.2 Docker Logging Driver ke Loki (Opsional)
Selain lewat Alloy yang menarik log dari Docker daemon secara terpusat, Docker juga menyediakan Loki logging driver resmi berupa plugin yang mengirim log satu container langsung ke Loki tanpa perantara Docker API, cocok dipakai kalau ingin mengurangi satu komponen (Alloy) dari stack. Instal plugin-nya terlebih dahulu di host Docker.
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissionsTag latest sengaja dipakai di sini, bukan mengikuti kebiasaan menghindari latest pada image biasa, karena rilis plugin ini didistribusikan per-arsitektur (misalnya amd64 dan arm64) alih-alih satu tag angka versi tunggal yang seragam, sesuai daftar tag resminya di Docker Hub. Tag latest tetap jadi cara paling aman untuk memastikan plugin yang terpasang cocok dengan arsitektur host tanpa perlu menebak nama tag spesifik-arsitektur secara manual.
Setelah plugin terpasang, konfigurasikan logging driver pada service aplikasi yang ingin log-nya dikirim langsung ke Loki.
services:
api:
build: ./api
logging:
driver: loki
options:
loki-url: "http://localhost:3100/loki/api/v1/push"
loki-retries: "3"
loki-batch-size: "400"Perlu dicatat, pendekatan logging driver ini cuma menangkap log dari container yang secara eksplisit dikonfigurasi memakainya, berbeda dari Alloy yang otomatis menangkap log seluruh container lewat service discovery. Kedua pendekatan sama-sama valid; pilih Alloy kalau ingin satu titik konfigurasi terpusat untuk seluruh container, atau logging driver kalau cuma sebagian container tertentu yang perlu dikirim ke Loki.
34.4 Integrasi Monitoring dengan Aplikasi Container
Metrik infrastruktur dari cAdvisor dan node-exporter memang berguna, tapi Developer maupun Sysadmin/DevOps Engineer di lapangan biasanya lebih membutuhkan metrik level aplikasi, seperti jumlah request per detik, latency tiap endpoint, atau jumlah error yang terjadi, karena metrik semacam ini jauh lebih cepat menunjukkan akar masalah dibanding sekadar mengetahui CPU container sedang tinggi.
34.4.1 Instrumentasi Aplikasi dengan Metrics Endpoint
Tambahkan library client Prometheus resmi sesuai bahasa yang dipakai backend, misalnya prom-client untuk Node.js, untuk mengekspos endpoint /metrics yang sudah dikonfigurasi pada prometheus.yml di bagian sebelumnya.
const client = require("prom-client");
const express = require("express");
const app = express();
const register = new client.Registry();
client.collectDefaultMetrics({ register });
const httpRequestDuration = new client.Histogram({
name: "http_request_duration_seconds",
help: "Durasi request HTTP dalam detik",
labelNames: ["method", "route", "status_code"],
});
register.registerMetric(httpRequestDuration);
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.end(await register.metrics());
});
app.listen(3000);collectDefaultMetrics otomatis mengekspos metrik standar seperti penggunaan memori proses Node.js dan durasi garbage collection, sementara metrik http_request_duration_seconds dibuat khusus untuk mengukur durasi tiap request berdasarkan label method, route, dan status_code, sesuai konvensi penamaan metrik yang direkomendasikan dokumentasi resmi Prometheus. Sambungkan port container api ke network observability-net supaya bisa dijangkau Prometheus, tanpa perlu mengekspos port tersebut ke host.
34.4.2 Alerting dengan Alertmanager
Sekadar melihat dashboard tidak cukup kalau tidak ada seseorang yang mengawasinya setiap saat. Alertmanager menangani notifikasi berdasarkan aturan alert yang dievaluasi Prometheus, mengirimkannya ke saluran seperti email, Slack, atau webhook lain. Tambahkan service alertmanager ke dalam stack.
services:
alertmanager:
image: prom/alertmanager:v0.34.0
volumes:
- ./alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports:
- "9093:9093"
networks:
- observability-netDefinisikan aturan alert di file terpisah, misalnya alert-rules.yml, lalu daftarkan lewat opsi rule_files pada prometheus.yml.
groups:
- name: container-alerts
rules:
- alert: ContainerHighCPU
expr: rate(container_cpu_usage_seconds_total{name!=""}[5m]) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "Container {{ $labels.name }} memakai CPU tinggi"
description: "Rata-rata pemakaian CPU container di atas 80% selama 5 menit terakhir."rule_files:
- "alert-rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]Aturan ContainerHighCPU ini memanfaatkan metrik container_cpu_usage_seconds_total yang berasal dari cAdvisor, memicu alert kalau rata-rata pemakaian CPU sebuah container di atas 80% selama minimal 5 menit berturut-turut, bukan cuma sekali lonjakan sesaat yang wajar terjadi. Klausa for: 5m ini penting supaya alert tidak terlalu sensitif terhadap fluktuasi normal, praktik yang lazim diterapkan Sysadmin/DevOps Engineer untuk mengurangi alert fatigue, yaitu kondisi tim jadi kebal atau mengabaikan notifikasi karena terlalu sering menerima alert yang ternyata tidak signifikan.
34.4.3 Verifikasi dan Troubleshooting Stack Observabilitas
Jalankan seluruh stack sekaligus, lalu periksa status tiap container untuk memastikan semuanya berjalan normal.
docker compose up -d
docker compose psBuka Grafana di http://localhost:3001 dan buat panel baru dengan query rate(container_cpu_usage_seconds_total[5m]) untuk memverifikasi metrik dari cAdvisor sudah masuk. Untuk memeriksa log, buka menu Explore di Grafana, pilih data source Loki, lalu jalankan query LogQL sederhana seperti {container="api"} untuk melihat log dari container aplikasi secara langsung tanpa perlu docker logs manual.
Kalau target Prometheus berstatus DOWN di halaman /targets, penyebab paling umum di lapangan adalah container target belum tergabung ke network yang sama dengan Prometheus, atau port yang didaftarkan di prometheus.yml tidak sesuai dengan port yang benar-benar di-listen aplikasi di dalam container. Kalau log tidak muncul di Loki lewat Alloy, periksa dulu apakah Alloy benar-benar punya akses ke docker.sock, karena kesalahan permission pada socket ini adalah penyebab paling sering ditemui saat komponen discovery.docker gagal menemukan container yang sedang berjalan. Buka antarmuka debugging Alloy di http://localhost:12345 untuk melihat status tiap komponen pipeline secara real-time, termasuk apakah loki.write berhasil mengirim data ke endpoint Loki atau justru gagal karena alamat url yang salah. Pastikan juga volume prometheus-data, grafana-data, dan loki-data tetap dipertahankan saat melakukan docker compose down tanpa opsi -v, karena riwayat metrik, dashboard, dan log yang sudah terkumpul akan hilang permanen kalau volume tersebut ikut terhapus.

