Mengapa observability adalah garis pemisah antara "jalan" dan "bisa dioperasikan": logging driver Docker beserta rotasinya yang mencegah disk penuh, HEALTHCHECK untuk deteksi dini kegagalan, metrics endpoint daemon, dan stack monitoring cAdvisor + Prometheus + Grafana yang siap produksi.

Setelah di episode 14 sebelumnya kita memastikan image yang masuk ke registry benar-benar bersih — di-scan kerentanannya dengan Trivy/Grype, memiliki SBOM, dan ditandatangani dengan Cosign — pada episode kali ini kita beralih dari apa yang masuk ke dalam sistem ke apa yang terjadi di dalamnya: observability. Image yang aman hanyalah titik awal; begitu berjalan di produksi, pertanyaan yang sesungguhnya menentukan adalah: apakah aplikasinya sehat? kalau tidak, kenapa? dan bagaimana kita tahu sebelum pengguna yang mengeluh?
Ini bukan barang mewah. Bayangkan kalian menjadi pilot yang diminta terbang tanpa indikator kecepatan, ketinggian, dan bahan bakar. Bisa saja penerbangannya mulus — sampai suatu hari ada yang salah dan kalian tidak punya petunjuk sama sekali. Kontainer berperilaku persis seperti itu: dia berjalan, dia mencetak log, dia bisa mati diam-diam. Tanpa alat baca, kalian hanya akan tahu ada masalah saat pengguna melaporkan error — dan pada saat itu, yang tersisa hanyalah tebak-tebakan.
Di episode ini kita akan membangun tiga lapis observability untuk kontainer: logging (mencatat apa yang terjadi), health & metrics (mengukur kondisi), dan audit event (mengetahui siapa melakukan apa). Kita mulai dari yang paling sering diabaikan namun paling sering menenggelamkan server — logging — lalu naik ke HEALTHCHECK, metrics endpoint daemon, dan menutup dengan stack monitoring standar industri: cAdvisor, Prometheus, dan Grafana.
Sebelum menyentuh perintah, sepakati dulu model mentalnya. Observability bukan satu fitur, melainkan tiga pertanyaan yang dijawab data:
Analogi yang tepat: log adalah black box pesawat, metrics adalah panel instrumen, dan healthcheck adalah pemeriksaan rutin sebelum lepas landas. Menjalankan produksi tanpa salah satunya sama seperti terbang dengan mata tertutup — bertahan, tapi setiap menitnya penuh risiko. Mari bangun ketiganya satu per satu.
Ketika aplikasi di dalam kontainer menulis ke stdout/stderr, data itu dicegat oleh logging driver Docker. Driver adalah plugin yang memutuskan ke mana baris-baris log itu dikirim. Standar bawaannya adalah json-file — setiap baris log ditulis ke file JSON di bawah /var/lib/docker/containers/<id>/. Perintah docker logs yang selama ini kita pakai sebenarnya hanya membaca file driver ini.
| Driver | Tujuan Log | Cocok untuk |
|---|---|---|
json-file | File JSON di host (default) | Single host, butuh docker logs |
local | File binary ringan di host | Performa tulis terbaik, tetap ada docker logs |
journald | Journal systemd | Host yang sudah memakai journald |
syslog | Server syslog lokal/remote | Aggregasi syslog lama |
gelf | Graylog / log ingest berbasis GELF | Centralized logging eksternal |
loki | Grafana Loki | Log + metrics dalam satu dashboard Grafana |
awslogs | Amazon CloudWatch Logs | Infrastruktur AWS |
Driver mana yang dipilih bukan soal selera — ia menentukan tiga hal: (1) apakah docker logs masih berfungsi, (2) ke mana log diagregasi saat ada puluhan host, dan (3) berapa cepat disk kalian penuh. Dua pertanyaan terakhir inilah yang akan menghantui kalian di produksi.
Secara arsitektur, bayangkan driver sebagai pipa air: json-file menampung air di bak mandi (host), loki/syslog/gelf meneruskannya ke tangki pusat (server log), dan journald menyambungkannya ke selokan kota (systemd host). Pilihan pipa menentukan ke mana air bisa mengalir.
Konfigurasi default bisa diubah global lewat /etc/docker/daemon.json — file yang dibaca daemon dockerd saat start (ingat sekilas dari episode 24 yang membedah daemon). Ini berlaku untuk semua kontainer yang dibuat setelahnya, kecuali yang secara eksplisit memilih driver sendiri:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Setelah mengubah daemon.json, restart daemon agar efeknya diterapkan:
sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'Dua opsi di log-opts itu bukan aksesoris — mereka adalah penyelamat disk yang paling diabaikan. max-size: "10m" memangkas file log setiap mencapai 10 MB, dan max-file: "3" menjaga hanya 3 file terakhir (total maksimum 30 MB per kontainer). Tanpa keduanya, json-file menulis tanpa henti — dan satu aplikasi yang mencetak log verbose bisa memakan ratusan GB dalam hitungan minggu. Kita akan bedah lebih dalam di bagian log rotation.
Kadang satu kontainer butuh driver berbeda dari kebijakan global — misalnya kontainer reverse proxy yang log aksesnya ingin dikirim ke loki, sementara sisanya tetap json-file. Driver bisa dipilih per kontainer saat docker run:
docker run -d --name web \
--log-driver=json-file \
--log-opt max-size=5m \
--log-opt max-file=2 \
-p 80:80 nginxdocker run -d --name app --log-driver=journald my-app:1.0
journalctl CONTAINER_NAME=app -fAturan praktisnya: set kebijakan global yang masuk akal di daemon.json, lalu override hanya pada kasus spesifik. Konsistensi driver penting karena aggregator log di sisi lain (Loki, Graylog, CloudWatch) harus bisa menerima format yang seragam dari semua host — driver campur-campur liar adalah resep log yang hilang.
Penting sekali memahami batas docker logs: ia hanya bekerja untuk driver yang menyimpan log lokal, yaitu json-file dan local. Begitu kalian memakai journald, syslog, loki, atau awslogs, docker logs menampilkan error "configured logging driver does not support reading". Log berpindah ke sistem tujuan — di journal, di server syslog, di CloudWatch — dan bukan lagi tanggung jawab Docker.
Ini keputusan desain yang masuk akal: kalau log sudah dikirim ke tempat lain, Docker tidak perlu menyimpan salinannya. Tapi kalian harus tahu dari awal, karena banyak tim "kehilangan" log saat migrasi driver — bukan lognya hilang, melainkan mereka mencari di tempat yang salah. Sebelum pindah driver, pastikan alur baca log baru sudah siap: journalctl -u docker.service untuk journald, atau query di server Loki/CloudWatch.
Penyebab paling umum produksi down bukan crash aplikasi, melainkan disk penuh karena log. Ceritanya selalu sama: sebuah service mencetak log berukuran besar, /var/lib/docker membengkak, satu partisi penuh, dan seluruh node ikut mati karena proses tidak bisa menulis. Ini insiden yang mengerikan karena terlihat seperti "server bermasalah" padahal akar masalahnya hanya satu baris konfigurasi yang tidak pernah disetel.
Mari kita lihat perilaku defaultnya dan bagaimana rotasi menyelamatkannya:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}Bayangkan mekanismenya seperti buku catatan. Tanpa rotasi, catatan ditulis sampai kertas habis lalu terus menumpuk di lantai (disk penuh). Dengan max-size, setiap kali catatan mencapai 10 MB, Docker menutup halaman itu, membuka halaman baru, dan menyimpan halaman lama — sampai max-file: 3 batas halaman yang disimpan. Halaman ke-4 menyingkirkan halaman pertama. Total jejak per kontainer dibatasi 30 MB, berapa pun lama kontainer hidup.
Warning
Log rotation dengan max-size dan max-file hanya didukung oleh driver json-file dan local. Driver journald, syslog, dan loki tidak menerima opsi ini — rotasinya menjadi tanggung jawab sistem tujuan (journald punya konfigurasi SystemMaxUse, syslog punya logrotate, dan seterusnya). Jangan mengandalkan rotasi Docker saat kalian berpindah driver.
Untuk memeriksa apakah rotasi benar-benar berjalan, lihat file log di host — harus ada beberapa file bernomor, bukan satu file raksasa:
sudo ls -lh /var/lib/docker/containers/$(docker ps -q --filter name=web)/*-json.logdocker logs menceritakan kejadian; docker stats menunjukkan pemakaian resource; tetapi keduanya tidak memberi tahu apakah aplikasi benar-benar berfungsi. Sebuah container bisa menghabiskan 0% CPU dan tetap sehat, atau menghabiskan 100% CPU dan masih melayani. Yang menentukan adalah: ketika diminta, apakah ia merespons dengan benar? Jawabannya datang dari HEALTHCHECK — perintah yang dijalankan daemon secara berkala untuk memverifikasi aplikasi.
Instruksi HEALTHCHECK di Dockerfile punya lima parameter:
--interval: jarak antar pengecekan (default 30s)--timeout: batas waktu satu pengecekan (default 30s)--retries: berapa kali gagal sebelum status menjadi unhealthy (default 3)--start-period: masa tenggang saat startup — healthcheck berjalan tapi kegagalan tidak dihitung (default 0s, wajib disetel untuk aplikasi yang butuh waktu inisialisasi)--start-interval: (versi baru) interval tersendiri selama start periodContoh nyata untuk web service — catat: kita memakai wget, bukan curl:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=20s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "server.js"]Bacanya: setiap 10 detik, coba akses /health; jika tidak berhasil dalam 5 detik, hitung sebagai kegagalan; 3 kegagalan berturut-turut → unhealthy. Selama 20 detik pertama (start period), kegagalan tidak dihitung agar aplikasi punya waktu memuat dependency-nya. Bisa juga memakai curl bila base image punya curl — perhatikan tautologi yang sering menggoda:
FROM nginx:alpine
HEALTHCHECK --interval=10s --timeout=3s --retries=2 \
CMD curl -f http://localhost/ || exit 1Tapi jangan buru-buru menulis curl di sembarang image. Ini jebakan klasik: image slim/alpine/distroless sering tidak menyertakan curl karena mengutamakan ukuran kecil. Jika kalian menulis HEALTHCHECK berbasis curl di image tanpa curl, healthcheck akan gagal bukan karena aplikasi sakit, melainkan karena curl-nya tidak ada — hasilnya: container yang sehat dikira mati. Solusinya: gunakan wget (lebih sering tersedia), atau gunakan tool yang memang ada di image (misalnya nginx -t), atau panggil endpoint via HTTP dari Node sendiri. Selalu verifikasi bahwa binary yang dipakai healthcheck benar-benar ada di image.
Untuk memeriksa status kesehatan:
docker inspect --format='{{json .State.Health}}' web | jq .
docker ps --filter health=unhealthydocker inspect akan menunjukkan riwayat pengecekan terakhir — output, exit code, dan stempel waktunya — sementara docker ps --filter health=unhealthy langsung memunculkan semua kontainer bermasalah. Inilah data yang dipakai orchestrator untuk restart otomatis (kita buktikan di episode 17), dan dipakai load balancer untuk berhenti mengirim traffic ke replika yang sakit.
Aplikasi sehat tidak berarti host sehat. Docker daemon sendiri — yang mengelola semua kontainer — harus dimonitor juga: berapa banyak kontainer berjalan, berapa image yang tersimpan, berapa lama operasi pull, dan seterusnya. Docker menyediakan endpoint metrics berformat Prometheus yang tinggal diaktifkan lewat daemon.json:
{
"metrics-addr": "127.0.0.1:9323",
"experimental": false
}sudo systemctl restart docker
curl -s http://127.0.0.1:9323/metrics | grep docker_container_runningImportant
Jangan pernah memaparkan metrics-addr ke antarmuka publik (misalnya 0.0.0.0:9323). Endpoint ini tidak punya autentikasi — siapa pun yang bisa menjangkaunya bisa membacakan semua metadata kontainer. Bind ke 127.0.0.1 dan biarkan Prometheus menjangkaunya dari dalam, atau sekat dengan network/firewall.
Sekarang kita rakit sistem monitoring yang sesungguhnya. Arsitekturnya membagi tiga tanggung jawab:
Berikut compose.yaml lengkap beserta konfigurasi Prometheus:
services:
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
ports:
- "8080:8080"
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
- /dev/disk/:/dev/disk:ro
privileged: true
devices:
- /dev/kmsg
restart: unless-stopped
node-exporter:
image: prom/node-exporter:v1.8.2
ports:
- "9100:9100"
restart: unless-stopped
prometheus:
image: prom/prometheus:v2.53.0
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
command:
- --config.file=/etc/prometheus/prometheus.yml
restart: unless-stopped
grafana:
image: grafana/grafana:11.1.0
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
restart: unless-stopped
volumes:
grafana-data:Jalankan dengan docker compose up -d, lalu akses: cAdvisor di http://<host>:8080, Prometheus di http://<host>:9090, dan Grafana di http://<host>:3000 (login admin/admin). Di Grafana, tambahkan datasource Prometheus (http://prometheus:9090) lalu import dashboard seperti "Docker monitoring" (ID 893) untuk gambaran container, dan "Node Exporter Full" (ID 1860) untuk kondisi host.
Perhatikan arsitekturnya: Prometheus men-scrape cAdvisor dan node-exporter berdasarkan nama service berkat DNS custom bridge (pelajaran episode 9). Tambahkan cAdvisor + node-exporter ke setiap node saat cluster Swarm nanti (episode 16-17) — begitu ada beberapa host, Prometheus cukup ditambahkan satu target per node untuk memantau semuanya.
Query Prometheus yang paling sering dipakai: container_cpu_usage_seconds_total untuk CPU container, container_memory_usage_bytes untuk memory, dan node_filesystem_avail_bytes untuk sisa disk host. Dengan cAdvisor + Prometheus + Grafana, kalian bisa menjawab "node mana yang akan kehabisan disk minggu depan?" sebelum jawabannya menghancurkan produksi.
Terakhir, event stream Docker adalah feed waktu nyata dari semua yang terjadi pada daemon: container dibuat, dimulai, dimatikan, image di-pull, volume dibuat, dan sebagainya. Ini berguna untuk debugging (mengapa container restart?) sekaligus audit keamanan (siapa menjalankan image apa kapan?):
docker events --filter type=container --filter event=restart
docker events --filter type=container --filter container=web --since 5m
docker events --format '{{.Time}} {{.Type}} {{.Action}} {{.Actor.Attributes.name}}'Dengan --since 5m kalian bisa merekonstruksi apa yang terjadi lima menit terakhir — misalnya, kenapa container tiba-tiba mati. docker events adalah "black box" yang jarang dipakai pemula tapi selalu dicari operator saat terjadi insiden.
Disk penuh karena log tanpa rotasi. Konfigurasi max-size + max-file seharusnya disetel sejak hari pertama — sebelum ada insiden, bukan sesudahnya. Cek rutin dengan docker system df (episode 4) adalah deteksi paling murah.
HEALTHCHECK memakai curl di image tanpa curl. Image slim/alpine/distroless sering tidak punya curl. Kegagalan healthcheck karena tool tidak ada akan menghasilkan false positive unhealthy. Gunakan wget atau binary yang memang ada di image, dan uji perintahnya di dalam container terlebih dahulu.
docker logs diam setelah ganti driver. Pindah ke journald/syslog/loki/awslogs membuat docker logs tidak lagi membaca apa pun. Ini bukan bug — log sudah dipindahkan. Siapkan jalur baca yang baru sebelum migrasi.
metrics-addr terbuka ke publik. Endpoint tanpa autentikasi; bind ke 127.0.0.1 atau network internal saja.
Prometheus scrapes sekali, lalu "tidak ada data". Pastikan job cadvisor dan node-exporter memakai nama service yang bisa di-resolve (DNS custom bridge) dan target tidak berubah-ubah. Di http://prometheus:9090/targets, periksa status up untuk tiap target.
Pada episode 15 ini kita telah membangun lapisan observability yang selama ini hilang: memahami bahwa logging driver menentukan ke mana log mengalir — json-file untuk lokal, journald/syslog/loki/awslogs/gelf untuk aggregasi — dan bahwa rotasi (max-size, max-file) adalah garis hidup disk di produksi. Kita juga menambahkan HEALTHCHECK agar daemon tahu aplikasi benar-benar merespons (dengan waspada terhadap image tanpa curl), mengaktifkan metrics endpoint daemon untuk memonitor mesinnya, merakit cAdvisor + Prometheus + Grafana yang memantau kontainer dan host sekaligus, dan memakai docker events sebagai jejak audit.
Inti yang harus kalian bawa:
daemon.json dari hari pertama; ini mencegah insiden disk penuh yang paling umum.docker logs hanya bekerja untuk driver json-file dan local — rencanakan jalur baca log sebelum pindah driver.--start-period dan binary yang benar-benar ada di image.127.0.0.1.Sekarang kalian bisa melihat satu host dengan jelas — log, status, dan metrik. Tapi bagaimana kalau aplikasi kalian tumbuh melampaui satu server, dan harus tersebar di beberapa mesin yang saling menggantikan saat rusak? Itulah titik di mana kita berhenti "menjalankan kontainer" dan mulai "mengorkestrasi". Di episode 16 selanjutnya kita akan mengaktifkan mode bawaan Docker untuk itu: Clustering & Orchestration dengan Docker Swarm Mode — menyatukan banyak node, memperkenalkan manager dan worker, dan memahami mengapa ada arsitektur cluster di balik sistem besar. Pastikan host kalian siap, karena kita akan membangun cluster sungguhan. Sampai jumpa di episode 16!