Lima service toko kini saling menyapa hanya dengan nama — proxy memanggil backend, backend memanggil db dan cache — dan selama semuanya sehat, tidak ada satu pun yang tahu alamat IP lawan bicaranya. Kenyamanan itu menyimpan pertanyaan yang belum pernah kita angkat secara sadar: siapa yang mencatat nama menjadi alamat, kapan catatan itu dibuat, dan apa yang terjadi pada aplikasi yang memegang alamat lama ketika container lawan bicaranya diciptakan ulang di alamat baru? Persoalan ini punya nama resmi — service discovery — dan mengabaikannya berarti mengandalkan keberuntungan setiap kali docker compose up berjalan. Chapter ini membongkar registry nama di balik Compose, membuktikan batas resolusinya per network, memberi database toko sebuah nama peran lewat network-alias, dan melengkapi backend dengan kebiasaan penutup: retry dengan backoff eksponensial yang membedakan ECONNREFUSED dari EAI_AGAIN.
Alamat yang Selalu Berganti #
Container toko bekerja seperti penyewa unit apartemen bulanan: setiap kali kontrak diperbarui — container di-recreate — unit yang ditempati bisa berubah, dan nomor lama tidak menunggu siapa pun. Ketika Budi menjalankan docker compose up -d setelah mengubah environment backend, container toko-backend-1 dibongkar dan diciptakan ulang; di network, alamat 172.18.0.4 yang lama dikembalikan ke kolam, dan container baru menerima alamat yang diberikan bendahara subnet saat itu — bisa alamat yang sama, bisa bukan. Aplikasi yang menulis alamat IP mentah di konfigurasinya akan terus mengetuk unit yang sudah kosong.
Masalah ini jauh lebih tua dari Docker, dan solusinya juga: pisahkan identitas dari alamat. Nama adalah identitas yang stabil dan mudah diingat manusia; alamat adalah detail sewa yang boleh berganti kapan saja; dan di antara keduanya berdiri sebuah peta yang selalu segar — service discovery, sistem yang menjawab pertanyaan “di alamat berapa db berada sekarang?”.
DNS Compose sebagai Registry Lokal #
Di lingkungan Compose, peta itu sudah terpasang tanpa kita pasang sendiri: embedded DNS pada alamat 127.0.0.11 yang kita bedah tuntas di Chapter Container DNS. Perilakunya sebagai service discovery bisa dibaca sebagai siklus registry:
- Registrasi terjadi saat container menyala — begitu
toko-db-1berjalan dibackend-net, daemon Docker menuliskan nama servicedb, nama containertoko-db-1, dan seluruh aliasnya ke tabel internal milik network itu. - Pencabutan terjadi saat container berhenti —
docker compose stop dbmenghapus entri itu seketika; namadbberhenti dijawab, dan pertanyaan berikutnya berakhir galat, bukan jawaban basi. - Pembacaan terjadi setiap kali koneksi baru dibuat — aplikasi yang menyambung ke
dbbertanya ke127.0.0.11dan menerima alamat yang berlaku saat itu.
Satu nuansa kerap mengagetkan pemilik stack: koneksi yang sudah terbuka tidak ikut pindah. Nama hanya dikonversi menjadi alamat pada saat koneksi dibuat; koneksi pool yang sudah terbentuk terus menuju alamat yang lama, walau container lawan sudah mati dan digantikan di alamat lain. Connection pool backend kita dapat memegang sepuluh koneksi ke db yang sudah tidak ada — kueri berikutnya akan gagal, dan hanya setelah kegagalan itu pool membuat koneksi baru, bertanya DNS lagi, dan mendapat alamat yang benar. Karena itu kebiasaan retry di bagian akhir chapter ini bukan pelengkap opsional, melainkan penutup celah antara registry yang segar dan koneksi yang sudah tua.
Verifikasi dari Dalam Container #
Klaim-klaim di atas layak dibuktikan, dan Compose menyediakan dua jalur uji yang keduanya penting. Jalur pertama menanyakan dari dalam service yang sedang berjalan:
$ docker compose exec backend getent hosts dbOutput yang Dihasilkan:
172.19.0.2 dbgetent menempuh jalur resolver sistem — jalur yang sama dipakai aplikasi Node.js saat pg menyambung ke db — sehingga satu baris ini adalah wujud yang dilihat aplikasi sungguhan. Jalur kedua memakai pola container debug: container sekali jalan yang dimasukkan ke network yang dituju hanya untuk mengintip keadaan, tanpa menyentuh service apa pun:
$ docker run --rm --network toko_backend-net alpine:3.20 getent hosts dbOutput yang Dihasilkan:
172.19.0.2 dbDua hasil yang sama dari dua penanya berbeda membuktikan bahwa registry melayani siapa pun yang berada di network yang tepat. Pola container debug ini layak dihafal: saat sebuah service tidak punya alat bantu di dalam imagenya — image produksi yang ramping sering tidak memuat nslookup apalagi curl — container Alpine sekali jalan adalah mikroskop paling murah untuk melihat network dari dalam.
Batas Resolusi per Network #
Keanggotaan network menentukan batas pengetahuan. Frontend toko menempel di frontend-net saja, sehingga frontend — atau siapa pun di network itu — tidak berhak mengetahui alamat db:
$ docker run --rm --network toko_frontend-net alpine:3.20 nslookup dbOutput yang Dihasilkan:
Server: 127.0.0.11
Address: 127.0.0.11:53
nslookup: can't resolve 'db': Name does not resolvePesan ini bukan galat yang harus diperbaiki — inilah isolasi by design yang kita rancang sejak chapter pertama: frontend tidak punya jalur menuju database, dan DNS dengan sopan menolak mengungkap alamat tetangga yang bukan anggota network yang sama. Diagnosa yang benar punya bukti: daftar anggota network itu sendiri.
$ docker network inspect toko_frontend-net --format '{{range .Containers}}{{.Name}} -> {{.IPv4Address}} {{end}}'Output yang Dihasilkan:
toko-proxy-1 -> 172.18.0.2 toko-frontend-1 -> 172.18.0.3 toko-backend-1 -> 172.18.0.4Mari kita telaah hasil pemeriksaan tersebut: db memang tidak terdaftar di frontend-net — penghuninya hanya proxy, frontend, dan backend yang menjadi jembatan; database hidup di backend-net bersama cache. Galat resolusi, daftar anggota, dan rancangan awal topologi kini saling menerangkan — begitulah cara membuktikan bahwa sebuah “galat” sebenarnya tembok yang sengaja dibangun.
Nama Peran dengan network-alias #
Nama service db memang cukup untuk stack kecil, tetapi ada satu lapis abstraksi lagi yang membuat arsitektur lebih lentur: nama peran. Alih-alih memanggil service dengan namanya, aplikasi memanggil peran yang dicarinya — “database utama” — dan siapa pun yang menyandang peran itu menjawab. Compose menyediakannya lewat aliases per network:
db:
networks:
backend-net:
aliases:
- db-utamaBackend kemudian mengganti alamat pada environment-nya:
DATABASE_URL: postgres://toko:rahasia123@db-utama:5432/toko$ docker compose up -dOutput yang Dihasilkan (dipotong):
[+] Running 7/7
✔ Container toko-db-1 Recreated 0.3s
✔ Container toko-db-1 Healthy 2.4s
✔ Container toko-backend-1 Recreated 0.2s
✔ Container toko-backend-1 Started 0.5s
...Dua Recreated pada output itu masing-masing beralasan: backend karena environment-nya berganti, dan db karena deklarasi networks-nya kini memuat alias — perubahan pada konfigurasi network memang menuntut container diciptakan ulang, dan data tetap aman di volume db-data. Verifikasi dari dalam backend memastikan nama peran benar-benar terdaftar:
$ docker compose exec backend getent hosts db-utamaOutput yang Dihasilkan:
172.19.0.2 db-utamaNilai praktisnya terlihat pada skenario yang belum terjadi tapi pasti datang: suatu hari db pindah mesin atau diganti dengan cluster baru. Dengan nama peran, migrasi menjadi urusan menyandangkan alias — db baru memakai aliases: [db-utama] di backend-net sementara db lama masih melayani sisanya, lalu digeser mulus tanpa satu aplikasi mengganti DATABASE_URL, tanpa rebuild image, tanpa environment baru. Pola ini adalah versi Compose dari --network-alias yang kita praktikkan di Chapter Network Alias, dengan satu tambahan penting: alias hidup di dalam deklarasi, ikut di-review, dan ikut berumur bersama berkas.
Retry dan Backoff Eksponensial #
Registry sekalipun tidak menjamin lawan bicara sedang siap — container bisa sedang menyala, sedang initdb, atau sedang restart. Aplikasi yang tahan banting memperlakukan kegagalan koneksi sebagai kejadian normal yang dijawab dengan menunggu lalu mencoba lagi, dan bentuk menunggu yang paling sopan kepada server adalah backoff eksponensial:
const jedaMaks = 8000;
const percobaanMaks = 10;
async function queryDenganRetry(sql, params, coba = 1) {
try {
return await pool.query(sql, params);
} catch (galat) {
if (coba >= percobaanMaks) throw galat;
const jeda = Math.min(500 * 2 ** (coba - 1), jedaMaks)
+ Math.random() * 250;
console.log(`percobaan ${coba} gagal, menunggu ${Math.round(jeda)} ms`);
await new Promise((selesai) => setTimeout(selesai, jeda));
return queryDenganRetry(sql, params, coba + 1);
}
}Mari kita telaah mekanisme tunggu tersebut:
500 * 2 ** (coba - 1)— jeda bergandeng dua tiap kegagalan: 500 ms, lalu 1, 2, 4, 8 detik;Math.min(..., jedaMaks)mematok plafon 8 detik sehingga percobaan-percobaan akhir tidak menjelma menjadi tidur panjang.percobaanMaks = 10— total jendela tunggu menembusi 50 detik, cukup luas untukinitdbpada volume segar atau restart policy yang sedang mengangkat db — dan setelah sepuluh kegagalan, galat dilempar; retry menolak menjadi lubang tanpa dasar.Math.random() * 250— jitter — sedikit acak yang sengaja ditaburkan; tanpa jitter, tiga replika backend yang gagal bersamaan akan mencoba ulang pada detik yang persis sama dan bersamaan pula menghantam db — derak serentak yang dikenal sebagai thundering herd.- Kegagalan memicu koneksi baru — di pustaka
pg, kueri yang gagal pada koneksi mati membuat pool membuang koneksi lama itu; percobaan berikutnya membuka koneksi baru, dan pembukaan itulah momen DNS ditanya ulang — di sinilah retry bertemu service discovery dan menutup celah koneksi tua dari bagian awal chapter.
Dua kode galat Node.js paling sering muncul di jalan ini dan layak dibedakan sejak kini. ECONNREFUSED berarti nama sudah berhasil menjadi alamat, tetapi tidak ada proses yang mendengarkan di alamat itu — db benar-benar mati, berhenti, atau masih sibuk initdb; gerbang service_healthy mencegah sebagian besar kasus ini, dan retry mengurus sisanya. EAI_AGAIN berarti langkah DNS sendiri yang gagal sementara — resolver di 127.0.0.11 belum menjawab pada saat aplikasi bertanya, kejadian sesaat yang lazim pada detik-detik pertama hidup sebuah container; biasanya cukup satu percobaan ulang singkat. Keduanya transient, keduanya ditangani backoff yang sama — yang membedakan hanyalah lapisan yang gagal, dan mengetahui lapisannya membuat log pagi Anda jauh lebih mudah dibaca.
Tiga Mekanisme Discovery #
Seluruh perjalanan section ini bertemu di satu tabel ringkas:
| Mekanisme | Cakupan | Kapan memadai |
|---|---|---|
| DNS Compose — nama service | satu host, satu project | stack lokal seperti toko kita |
network-alias — nama peran | satu network | abstraksi peran, migrasi dan penggeseran mulus |
| Orchestrator — Kubernetes Service, Consul, etcd | banyak host | klaster produksi lintas mesin |
Baris pertama gratis dan sudah bekerja untuk toko; baris kedua menambah satu lapis kebebasan tanpa biaya operasional; baris ketiga adalah pintu menuju dunia yang menanti di Section Orchestration dan Ekosistem, tempat Compose menyerahkan kendali.
Nama kini menyelesaikan, koneksi tua digantikan retry, dan topologi toko menjelaskan dirinya sendiri. Tetapi satu kegelisahan masih tersisa dari seluruh section ini: urutan hidup. Kita telah memakai depends_on dengan tiga kondisi berbeda — service_started, service_healthy, service_completed_successfully — tapi belum pernah menyusun semuanya menjadi satu kebijakan startup yang utuh untuk stack lima service, dan belum pernah membahas apa yang sebaiknya terjadi saat stack dimatikan dengan sopan. Chapter Startup dan Dependency merampungkan bagian itu: urutan menyala yang dijaga gerbang kesehatan, service one-shot pada tempatnya, dan warisan sinyal yang harus dihormati aplikasi sebelum lampu dipadamkan.
Referensi #
- Docker networking — embedded DNS, resolusi per network, dan perilaku user-defined network.
- networks — Compose File Reference
—
aliasesper service di dalam network Compose. - Kubernetes Documentation — konsep Service sebagai service discovery lintas host.