Aplikasi yang berjalan di container mudah terasa seperti kotak tertutup: prosesnya hidup di namespace sendiri, filesystemnya tersusun dari layer image, dan tidak ada terminal yang bisa kita buka begitu saja. Keyakinan itu hanya bertahan sampai kegagalan pertama tiba, karena Docker sejak awal membangun jendela-jendela kecil pada setiap kotak — logs mengalirkan apa yang dikatakan aplikasi, exec membuka pintu ke dalam, dan inspect membocorkan arsip keadaan container tanpa meminta aplikasinya bangun. Debugging container pada dasarnya adalah seni mengetuk jendela yang tepat untuk gejala yang tepat. Chapter ini menelusuri stack toko — proxy, backend Node.js yang mendengarkan port 3000, dan PostgreSQL dengan kredensial toko/rahasia123 — saat empat kecelakaan klasik datang berturut-turut: aplikasi yang mati, berkas yang hilang, memori yang habis, dan bug logika yang hanya muncul di jalur tertentu.
Kasus 1: Aplikasi Mati Saat Permintaan Pertama #
Kasus pembuka datang dari rutinitas paling biasa. Budi menarik perubahan terbaru Siti — fitur diskon yang membaca parameter diskon pada rute /produk.json — lalu menyinkronkan stack dengan kebiasaan up -d --build dari Chapter Image, Build, dan Pull. Stack menyala tanpa keluhan, tetapi beberapa detik kemudian curl dari browser Siti menggantung tanpa jawaban, dan inilah yang dilihat Budi:
$ docker compose psOutput yang Dihasilkan:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
toko-proxy-1 nginx:1.29-alpine "/docker-entrypoint.…" proxy About an hour ago Up About an hour 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
toko-frontend-1 nginx:1.29-alpine "/docker-entrypoint.…" frontend About an hour ago Up About an hour 80/tcp
toko-backend-1 toko-api:dev "docker-entrypoint.s…" backend 2 minutes ago Exited (1) 2 minutes ago 3000/tcp
toko-db-1 postgres:18-alpine "docker-entrypoint.s…" db About an hour ago Up About an hour (healthy) 5432/tcpBaris backend adalah satu-satunya yang tidak berdiri — Exited (1), kode kegagalan aplikasi dari konvensi yang kita bedah pada Chapter Memahami Exit Code. Jendela pertama yang layak diketuk adalah log:
$ docker compose logs backendOutput yang Dihasilkan (dipotong):
toko-backend-1 | toko-api mendengarkan di port 3000
toko-backend-1 | TypeError: Cannot read properties of undefined (reading 'harga')
toko-backend-1 | at hitungDiskon (/app/server.js:42:34)
toko-backend-1 | at /app/server.js:61:21
toko-backend-1 | at Layer.handle (/app/node_modules/express/lib/router/layer.js:158:17)
toko-backend-1 | at next (/app/node_modules/express/lib/router/route.js:135:11)
toko-backend-1 | ...
toko-backend-1 | Node.js v22.20.0Mari kita telaah jejak yang ditinggalkan log tersebut:
- Aplikasi sempat sehat selama dua baris pertama. Startup line tercetak normal, galat baru muncul ketika permintaan pertama dengan parameter
diskonmasuk — pola khas bug yang hidup di jalur handler, bukan di jalur startup. - Stack trace menyebut alamat persisnya. Fungsi
hitungDiskonpada/app/server.jsbaris 42 mencoba membaca propertihargadari nilaiundefined; Siti lupa memvalidasi daftar kosong yang dikirimkan toko kecil pertama yang belum punya produk terdaftar. Node.js v22.20.0di baris terakhir menandai proses yang tumbang. Galat tidak tertangani pada handler Express meruntuhkan seluruh proses, container keluar dengan kode 1, dan karena tidak ada restart policy pada backend, ia tinggalExitedselamanya — kombinasi perilaku yang sudah kita saksikan pada Chapter depends_on dan Startup Order.- Log tetap terbaca setelah container mati. Driver
json-filemenyimpan riwayat keluaran proses pada berkas di host, sehingga penyelidikan tidak kehilangan bukti hanya karena korban sudah tiada — perilaku yang kita bedah pada Chapter Melihat Log Container.
Perbaikannya satu baris pengaman: hitungDiskon mengembalikan nilai tanpa diskon bila daftar produk kosong. Karena kode backend hidup di dalam image — toko memang belum memakai bind mount source code — perbaikan menuntut satu kali sinkronisasi:
$ docker compose up -d --buildOutput yang Dihasilkan (dipotong):
[+] Building 8.3s (14/14) FINISHED
...
=> => naming to docker.io/library/toko-api:dev 0.0s
[+] Running 4/4
✔ Container toko-db-1 Healthy
✔ Container toko-migrate-1 Exited (0)
✔ Container toko-backend-1 Recreated
✔ Container toko-backend-1 Started$ curl "localhost:8080/api/produk.json?diskon=10"Output yang Dihasilkan (dipotong):
[{"id":1,"nama":"Kopi Arabika Gayo","harga":76500},{"id":2,"nama":"Teh Melati Putih","harga":108000},...]Kasus pertama selesai dengan pola kerja tiga langkah yang akan berulang sepanjang chapter: lihat status di ps, dengar ceritanya di logs, lalu perbaiki dan sinkronkan. Delapan puluh persen debugging container memang hanyalah dua langkah pertama.
Kasus 2: Berkas Konfigurasi yang Tidak Pernah Sampai #
Kasus kedua lebih diam-diam. Beberapa hari setelah fitur diskon, tim toko memindahkan pengaturan diskon default dan zona waktu ke berkas config/pengaturan.json yang dibaca backend saat setiap permintaan diskon tiba — dan berkas itu dipasang lewat bind mount, pola yang kita bangun pada Chapter Bind Mount Source Code. Setelah up -d, backend hidup, tetapi setiap permintaan diskon dijawab galat server. Log memperlihatkan keluhan yang panjang dan berulang:
$ docker compose logs --tail=5 backendOutput yang Dihasilkan:
toko-backend-1 | galat membaca pengaturan: Error: ENOENT: no such file or directory, open '/app/config/pengaturan.json'
toko-backend-1 | galat membaca pengaturan: Error: ENOENT: no such file or directory, open '/app/config/pengaturan.json'
toko-backend-1 | galat membaca pengaturan: Error: ENOENT: no such file or directory, open '/app/config/pengaturan.json'Berkas itu ada di host — Budi baru saja menuliskannya — jadi dugaan pertama jatuh pada mount yang tidak bekerja. Jendela kedua yang layak diketuk adalah mata kita sendiri di dalam container:
$ docker compose exec backend ls -la /app/configOutput yang Dihasilkan:
total 8
drwxr-xr-x 2 root root 4096 Oct 15 09:12 .
drwxr-xr-x 1 root root 4096 Oct 15 09:12 ..Direktori /app/config ada tetapi kosong melompong — bahkan pengaturan.json yang seharusnya menembus bind mount tidak pernah tiba. Perhatikan pula pemiliknya: root:root, padahal Budi menulis berkas sebagai user biasa di laptopnya. Pemeriksaan berikutnya dilakukan dari sisi host:
$ lsOutput yang Dihasilkan:
api compose.yaml config frontend-html konfig nginx.confMari kita telaah bukti-bukti yang barusan terkumpul:
- Folder
configdankonfigberdiri berdampingan di host. Entri bind mount pada compose.yaml tertulis./konfig:/app/config— satu huruf salah — sementara berkas pengaturan yang Budi tulis berada di folderconfigyang benar. - Docker diam-diam melahirkan folder typo itu. Bentuk pendek bind mount membuat direktori sumber yang tidak ada saat container diciptakan, perilaku yang sudah kita saksikan sebagai peringatan pada Chapter Bind Mount — folder
konfiglahir kosong dan dimiliki root, persis seperti isi/app/configyang kita lihat. - Mount bekerja; isinya yang salah. Path target memang ter-mount, tetapi yang termount adalah folder kosong hasil typo, bukan folder berisi
pengaturan.json— mount yang “berhasil” membuat galat ini tidak pernah bersuara. - Inilah wajah klasik kesalahan bind mount: tidak ada pesan galat dari Docker. Aplikasi dibiarkan membaca direktori kosong; diagnosis hanya bisa datang dari membandingkan isi host dengan isi container, dan
exec lsadalah mata yang paling murah.
Perbaikannya menegakkan ejaan yang benar pada compose.yaml:
backend:
volumes:
- ./config:/app/configBudi menghapus folder konfig yang lahir dari kecelakaan, menjalankan docker compose up -d agar container dibuat ulang dengan mount yang benar, lalu memastikan berkasnya sungguh tiba:
$ docker compose exec backend cat /app/config/pengaturan.jsonOutput yang Dihasilkan:
{"diskonDefault":10,"zonaWaktu":"Asia/Jakarta"}Kasus 3: Kematian Tanpa Pesan Perpisahan #
Kasus ketiga tidak meninggalkan pesan sama sekali. Laporan rekap bulanan baru di backend memuat seluruh tabel pesanan ke memori sebelum menghitung totalnya, dan setiap kali Siti membuka halaman rekap, backend mati beberapa menit kemudian — tanpa satu baris galat di log. Kolom STATUS pada korban menyimpan petunjuk pertama:
$ docker compose ps -a backendOutput yang Dihasilkan:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
toko-backend-1 toko-api:dev "docker-entrypoint.s…" backend 8 minutes ago Exited (137) 3 minutes ago 3000/tcpAngka 137 adalah kode yang sudah kita hafal dari Chapter Memahami Exit Code: 128 + 9, proses dijatuhkan SIGKILL. Pelaku SIGKILL ada dua — tangan manusia lewat docker kill, atau OOM killer milik kernel — dan kompos yang membedakannya tersimpan rapi di inspect:
$ docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' toko-backend-1Output yang Dihasilkan:
true 137Nilai true pada field .State.OOMKilled adalah pengakuan resmi: kernel membunuh proses backend karena cgroup-nya kehabisan memori. Pagar yang membuatnya mungkin adalah keputusan tim minggu lalu — mem_limit: 256m pada service backend di compose.yaml, untuk melindungi laptop bersama dari aplikasi yang kebablasan. Siti menyalakan ulang backend lalu memantau kamera resource sambil membuka halaman rekap sekali lagi:
$ docker stats --no-streamOutput yang Dihasilkan:
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
1c9f8e2b7a4d toko-backend-1 38.15% 254.1MiB / 256MiB 99.26% 3.4MB / 0B 118MB / 0B 11
6b2d4f8a1e3c toko-db-1 2.03% 41.2MiB / 7.63GiB 0.54% 1.2MB / 12MB 88.3MB / 0B 9Mari kita telaah ketiga bukti yang mengunci kasus ini:
254.1MiB / 256MiBdenganMEM %99.26. Aplikasi memburu langit-langit cgroup-nya bar demi bar selama rekap berjalan; beberapa saat setelah potret ini diambil, angka itu menyentuh 256MiB dan kernel menjatuhkanSIGKILL— drama yang persis kita provocate secara sengaja pada Chapter CPU dan Memory Limit.- Tidak ada log karena tidak ada aplikasi yang sempat bicara.
SIGKILLtidak bisa ditangkap proses mana pun; backend tidak diberi kesempatan menutup koneksi apalagi mencetak pesan perpisahan — sifat sinyal yang kita bahas tuntas pada chapter yang sama. - Dua jalan perbaikan yang berbeda kelas. Jangka pendek: menaikkan
mem_limitpada compose.yaml. Jangka panjang: memperbaiki cara kerja rekap agar tidak memuat seluruh tabel sekaligus. Mekanisme lengkap memori, swap, dan dampaknya pada keputusan angka adalah materi Chapter Memory Limit dan Swap di Section Manajemen Resource — sekarang kita cukup tahu bahwa diagnosis OOM berdiri di atas tiga perintah:psuntuk kode,inspectuntuk pengakuan,statsuntuk rekaman kejahatan.
Kasus 4: Turun ke Dalam Container #
Kasus keempat adalah misteri klasik yang paling menyebalkan: backend hidup, database sehat, tetapi jawaban dari proxy tersendat-sendat, dan dugaan jatuh ke koneksi antar container. Untuk kasus seperti ini, tidak ada pengganti bagi mata kita sendiri di dalam kotak — pintunya dibuka lewat sesi shell interaktif:
$ docker compose exec backend shOutput yang Dihasilkan (dipotong):
/app $ env
DATABASE_URL=postgres://toko:rahasia123@db:5432/toko
NODE_VERSION=22.20.0
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
HOME=/home/node
/app $ ps aux
PID USER TIME COMMAND
1 node 0:01 node server.js
47 node 0:00 ps aux
/app $ nc -zv db 5432
db (172.19.0.2:5432) open
/app $ wget -qO- localhost:3000/produk.json
[{"id":1,"nama":"Kopi Arabika Gayo","harga":85000},{"id":2,"nama":"Teh Melati Putih","harga":120000},{"id":3,"nama":"Gula Aren Cair","harga":45000}]
/app $ exitMari kita telaah empat pemeriksaan dalam sesi tersebut:
envmembuktikan apa yang benar-benar diterima proses.DATABASE_URLmenunjuk nama servicedb— bukanlocalhost— sesuai peringatan kebiasaan yang kita paku sejak Chapter Port dan Network; nilaiNODE_VERSIONmengonfirmasi basis image yang dipakai container.ps auxmemperlihatkan isi rumah. Persis satu proses hidup:node server.jssebagai PID 1, tanpa shell perantara — bentuk exec form yang membuat sinyal sampai ke aplikasi, dan pengingat bahwadocker compose topdari Chapter Menjalankan dan Mengelola Stack menampilkan tabel yang sama dari luar.nc -zv db 5432menguji jalur yang dipakai aplikasi. Koneksi TCP ke port PostgreSQL ditempuh dari DALAM container, melalui networktoko_backend-net, dengan resolusi nama oleh embedded DNS di127.0.0.11— jalur yang sama persis dengan yang dilalui pustakapg; jawabanopenberarti jalur tersebut sehat, sehingga dugaan konektivitas gugur. Inilah latihan inti Chapter Komunikasi Antar Container, dikerjakan dari dalam korban.wget -qO- localhost:3000/produk.jsonmemeriksa aplikasi dari balik pintu. Permintaan tidak melewati proxy di port host 8080 melainkan mengetuk port 3000 langsung dari dalam container; jawaban JSON yang utuh membebaskan backend dari tuduhan. Kesendatan ternyata berasal dari proxy yang keliru menyimpan alamat backend lama — satudocker compose restart proxymenuntasknya.
Kotak Perkakas Sementara #
Kasus keempat berjalan mulus karena image node:22-alpine membawa busybox beserta nc dan wget-nya. Namun image yang benar-benar ramping — distroless, atau varian scratch dari Chapter Multi-Stage Build — tidak membawa shell sama sekali, dan docker compose exec backend sh akan gugur sebelum dimulai. Jalan keluarnya bukan menjejalkan alat ke dalam image aplikasi, melainkan mendatangkan kotak perkakas ke network yang sama:
$ docker run --rm -it --network toko_backend-net alpine:3.20 shOutput yang Dihasilkan (dipotong):
/ # apk add curl
fetch https://dl-cdn.alpinelinux.org/alpine/v3.20/main/x86_64/APKINDEX.tar.gz
...
OK: 9 MiB in 7 packages
/ # curl -s http://backend:3000/produk.json | head -c 120
[{"id":1,"nama":"Kopi Arabika Gayo","harga":85000},{"id":2,"nama":"Teh Melati Putih","harga":120000},{"id":3,"n
/ # exitMari kita telaah pola container perkakas tersebut:
--network toko_backend-netmeletakkan alat satu hop dari seluruh service. Container alat menjadi anggota network yang sama dengan backend dan db, sehingga nama service resolve dan setiap port internal terjangkau — nama network ini mengikuti pola<project>_<network>yang kita kenal sejak Section Docker Compose.apk addmemasang hanya untuk container ini. Alat terpasang di writable layer container yang akan dibuang--rm; imagealpine:3.20dan image aplikasi tidak tersentuh sama sekali — kebalikan dari memasangcurlke Dockerfile demi satu sesi diagnosa.- Netshoot membungkus pola ini menjadi satu image. Pihak ketiga mengemas puluhan peralatan diagnosa jaringan —
dig,tcpdump,mtr,iperf— dalam image bernama netshoot, yang sudah pernah kita panggil sebagai service ber-profildebugpada Chapter Profiles; memasukkannya ke network project memberi seluruh kotak perkakas tanpa satu paket pun menempel pada aplikasi.
Menempelkan Debugger Node #
Empat kasus di atas semuanya terdiagnosis dari luar. Untuk bug logika yang hanya muncul pada jalur tertentu — syarat yang tidak terpenuhi, urutan yang tidak terduga — alat yang tepat adalah debugger sungguhan yang menempel ke proses Node. Node.js membuka protokol inspectornya lewat satu flag, dan port 9229 yang kita EXPOSE pada target dev kini menemukan pemakaiannya. Budi menukar perintah service web sementara pada compose.override.yaml miliknya:
services:
web:
command: node --inspect=0.0.0.0:9229 src/server.js
ports:
- "9229:9229"Dua kunci itu bekerja di atas perilaku penggabungan berkas override yang kita bedah pada Chapter Compose Override dan Environment: command mengganti total perintah bawaan image, sementara ports digabung sebagai union — 9229 diterbitkan di samping 8080 tanpa saling menyingkirkan. Dari sisi editor, VS Code memasang debugger-nya lewat konfigurasi attach pada launch.json:
{
"type": "node",
"request": "attach",
"name": "katalog-web di container",
"address": "localhost",
"port": 9229,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/app"
}Sesi debug kemudian berjalan seperti debugging lokal biasa: breakpoint dipasang pada src/server.js, permintaan dikirim, dan eksekusi proses di dalam container berhenti tepat di baris yang ditandai — localRoot dan remoteRoot yang memetakan folder host ke /app membuat baris breakpoint jatuh ke tempat yang benar. Hot reload tidak harus dikorbankan: nodemon --inspect=0.0.0.0:9229 src/server.js menggabungkan watcher dan inspector dalam satu perintah. Setelah sesi selesai, kedua kunci tambahan itu dibuang dari override dan stack kembali ke kebiasaan normalnya.
Peta Gejala ke Alat #
Empat kasus hari ini cukup untuk menyusun peta lengkap. Tabel berikut memasangkan gejala yang paling sering dijumpai dengan jendela pertama yang layak diketuk:
| Gejala yang terlihat | Alat pertama | Dibedah tuntas di |
|---|---|---|
| Container Restarting atau Exited | docker compose ps lalu docker compose logs | Memahami Exit Code |
| Berkas yang diharapkan tidak ada | docker compose exec dengan ls dan cat | Bind Mount Source Code |
| Exited (137) tanpa satu baris log | docker inspect .State.OOMKilled dan docker stats | Memory Limit dan Swap |
| Koneksi antar service tersendat | nc -zv dari dalam container, kotak perkakas, netshoot | Komunikasi Antar Container |
| Bug logika yang tak terlihat di log | node --inspect dengan VS Code attach | chapter ini |
Seluruh perangkat ini punya satu kesamaan yang tidak nyaman: semuanya bekerja setelah kegagalan terjadi. Kita memburu galat yang sudah berumur, membaca jejak yang sudah dingin, menambal kode yang sudah lulus ke tangan pengguna development lain. Pergeseran sikap yang menutup section ini adalah bergerak sebelum kegagalan lahir — menjalankan test secara rutin di dalam container, di lingkungan yang identik dengan produksi, sehingga bug ditemukan saat masih murah. Unit test lewat docker compose run, integration test dengan database sekali pakai di atas tmpfs, dan exit code yang menjadi kontrak bagi pipeline — itulah agenda Chapter Menjalankan Test di Container.
Referensi #
- docker compose logs
— flag
--tail,--follow, dan pemfilteran log per service. - docker container exec — menjalankan perintah dan shell interaktif pada container yang hidup.
- docker container inspect
— membaca field
.State.OOMKilleddan.State.ExitCodelewat template. - docker container stats — arti kedelapan kolom pemantauan resource secara langsung.