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:

  1. Registrasi terjadi saat container menyala — begitu toko-db-1 berjalan di backend-net, daemon Docker menuliskan nama service db, nama container toko-db-1, dan seluruh aliasnya ke tabel internal milik network itu.
  2. Pencabutan terjadi saat container berhenti — docker compose stop db menghapus entri itu seketika; nama db berhenti dijawab, dan pertanyaan berikutnya berakhir galat, bukan jawaban basi.
  3. Pembacaan terjadi setiap kali koneksi baru dibuat — aplikasi yang menyambung ke db bertanya ke 127.0.0.11 dan 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:

bash
$ docker compose exec backend getent hosts db

Output yang Dihasilkan:

text
172.19.0.2      db

getent 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:

bash
$ docker run --rm --network toko_backend-net alpine:3.20 getent hosts db

Output yang Dihasilkan:

text
172.19.0.2      db

Dua 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:

bash
$ docker run --rm --network toko_frontend-net alpine:3.20 nslookup db

Output yang Dihasilkan:

text
Server:   127.0.0.11
Address:  127.0.0.11:53

nslookup: can't resolve 'db': Name does not resolve

Pesan 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.

bash
$ docker network inspect toko_frontend-net --format '{{range .Containers}}{{.Name}} -> {{.IPv4Address}} {{end}}'

Output yang Dihasilkan:

text
toko-proxy-1 -> 172.18.0.2 toko-frontend-1 -> 172.18.0.3 toko-backend-1 -> 172.18.0.4

Mari 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:

yaml
  db:
    networks:
      backend-net:
        aliases:
          - db-utama

Backend kemudian mengganti alamat pada environment-nya:

yaml
      DATABASE_URL: postgres://toko:rahasia123@db-utama:5432/toko
bash
$ docker compose up -d

Output yang Dihasilkan (dipotong):

text
[+] 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:

bash
$ docker compose exec backend getent hosts db-utama

Output yang Dihasilkan:

text
172.19.0.2      db-utama

Nilai 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:

js
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:

  1. 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.
  2. percobaanMaks = 10 — total jendela tunggu menembusi 50 detik, cukup luas untuk initdb pada volume segar atau restart policy yang sedang mengangkat db — dan setelah sepuluh kegagalan, galat dilempar; retry menolak menjadi lubang tanpa dasar.
  3. 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.
  4. 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:

MekanismeCakupanKapan memadai
DNS Compose — nama servicesatu host, satu projectstack lokal seperti toko kita
network-alias — nama peransatu networkabstraksi peran, migrasi dan penggeseran mulus
Orchestrator — Kubernetes Service, Consul, etcdbanyak hostklaster 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 #