Service web kini tahu persis cara mencapai db lewat DATABASE_URL, dan di mesin Budi stack berjalan mulus hari demi hari. Ceritanya berubah begitu Siti ikut serta: ia menyalin repositori katalog ke mesinnya yang masih segar, menyalin .env.example menjadi .env sesuai pola chapter sebelumnya, lalu menjalankan docker compose up — dan web mati beberapa detik kemudian dengan galat koneksi yang tidak pernah Budi lihat. Inilah masalah klasik multi-service: container db memang dinyalakan, tetapi db yang menyala belum tentu db yang siap, karena inisialisasi PostgreSQL memakan waktu beberapa detik pada volume yang masih segar. Chapter ini membedah cara Compose mengatur urutan start lewat kunci depends_on — mulai dari bentuk daftarnya yang sederhana, bentuk petanya yang bisa menunggu kesehatan, sampai batas paling pentingnya: urutan start tetap bukan jaminan kesiapan aplikasi.

Galat yang Datang dan Pergi #

Mari kita lihat dulu tampang masalahnya di mesin Siti. Stack menyala tanpa keluhan:

bash
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog_default   Created
 ✔ Container katalog-db-1    Started
 ✔ Container katalog-web-1   Started
 ✔ Container katalog-web-1   Exited

Baris terakhir itu kabar duka yang baru muncul belasan detik kemudian. Keadaan stack terbaca jelas lewat docker compose ps:

bash
$ docker compose ps

Output yang Dihasilkan:

text
NAME           IMAGE               COMMAND                  SERVICE   CREATED          STATUS                    PORTS
katalog-db-1   postgres:18-alpine  "docker-entrypoint.s…"   db        58 seconds ago   Up 57 seconds             5432/tcp
katalog-web-1  katalog-web         "docker-entrypoint.s…"   web       58 seconds ago   Exited (1) 52 seconds ago

Log container web memperlihatkan biang keroknya:

bash
$ docker compose logs web

Output yang Dihasilkan (dipotong):

text
katalog-web-1  | node:internal/process/promises:396
katalog-web-1  |   triggerUncaughtException(err, true /* fromPromise */);
katalog-web-1  |   ^
katalog-web-1  |
katalog-web-1  | Error: connect ECONNREFUSED 172.18.0.2:5432
katalog-web-1  |     at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1634:16)
katalog-web-1  |     ...
katalog-web-1  |   errno: -111, code: 'ECONNREFUSED', syscall: 'connect',
katalog-web-1  |   address: '172.18.0.2', port: 5432
katalog-web-1  |
katalog-web-1  | Node.js v22.20.0

Mari kita telaah rangkaian kejadian di atas:

  1. ECONNREFUSED — sistem operasi menolak koneksi karena tidak ada satu proses pun yang sedang mendengarkan di 172.18.0.2:5432; alamat itu adalah container db di dalam network katalog_default, tepat sasaran dari DATABASE_URL.
  2. Container db memang Up — proses utamanya berjalan, tetapi volumenya masih segar sehingga entry point PostgreSQL sibuk menjalankan initdb; server database belum membuka socket untuk menerima koneksi siapa pun.
  3. web keluar dengan exit code 1 — Node.js tidak menemukan handler untuk galat koneksi, prosesnya berhenti, dan tanpa restart policy container itu tinggal status Exited (1) selamanya, mekanisme exit code yang kita bedah pada Chapter Memahami Exit Code.
  4. Di mesin Budi galat ini tak pernah tampak — volume katalog_db-data yang sudah terisi membuat initdb dilewati, sehingga db siap hampir seketika; hasil yang bergantung kecepatan mesin dan umur volume seperti inilah yang disebut race condition, dan justru sifatnya yang kadang-hilang-kadang-tidak membuatnya sulit diburu.

depends_on Bentuk Daftar: Urutan Start Saja #

Kunci depends_on adalah cara service menyatakan bahwa dirinya butuh service lain lebih dulu. Bentuk paling sederhana berupa daftar nama service:

yaml
  web:
    build: .
    ports:
      - "8080:3000"
    env_file:
      - .env
    environment:
      DATABASE_URL: postgres://katalog:${POSTGRES_PASSWORD}@db:5432/katalog
    depends_on:
      - db

Dengan deklarasi ini Compose menjamin satu hal: container db dinyalakan lebih dulu, dan container web baru diciptakan setelah status container db mencapai Started. Sayangnya Started hanya berarti proses utama sudah berjalan — tidak ada janji apa pun bahwa PostgreSQL sudah mendengarkan port 5432. Pada mesin Siti, up kedua tampak berhasil karena db kebetulan sudah lama siap; tetapi ketika mereka mengulang siklus down dan up beberapa kali, pada putaran ketiga web kembali mati dengan ECONNREFUSED yang sama. Urutan start yang benar ternyata masih menyisakan jendela race condition di dalamnya.

depends_on Bentuk Peta dengan condition #

Bentuk kedua depends_on memperbolehkan kita menuntut lebih banyak dari sekadar urutan. Service dependensi ditulis sebagai kunci, dan kondisinya dinyatakan gamblang:

yaml
    depends_on:
      db:
        condition: service_healthy

Tiga kondisi yang tersedia membentuk tangga ketegasan:

  1. service_started — perilaku yang sama dengan bentuk daftar: tunggu container dependensi berjalan; ini nilai bawaan bila condition tidak ditulis.
  2. service_healthy — tunggu container dependensi berstatus healthy, status yang hanya bisa dicapai lewat healthcheck; inilah kondisi yang kita pakai untuk db.
  3. service_completed_successfully — tunggu service dependensi selesai dan keluar dengan exit code 0; kondisi ini lahir untuk service one-shot seperti job migrasi database yang harus tuntas sebelum aplikasi menyala — pola lengkapnya kita praktikkan pada Chapter Startup dan Dependency di Section Multi-Container Application.

healthcheck untuk Service db #

Kondisi service_healthy hanya bermakna bila db punya healthcheck — tanpa probe, status healthy tidak akan pernah tercapai dan web menunggu selamanya. Compose menempelkan healthcheck langsung pada service, tanpa menyentuh Dockerfile image:

yaml
  db:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: katalog
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: katalog
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U katalog -d katalog"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
      start_interval: 2s

Mari kita telaah enam kunci blok healthcheck di atas:

  1. test — perintah probe yang dijalankan di dalam container; pg_isready adalah utilitas resmi PostgreSQL yang keluar dengan exit code 0 hanya bila server benar-benar menerima koneksi, dan CMD-SHELL menjalankannya melalui shell container.
  2. interval: 5s — jeda antara dua pemeriksaan; angka kecil membuat status berubah responsif.
  3. timeout: 3s — batas waktu tiap probe; probe yang menggantung lebih lama dari ini dianggap gagal.
  4. retries: 5 — jumlah kegagalan beruntun sebelum status container berubah menjadi unhealthy.
  5. start_period: 10s — masa hangar di awal; kegagalan probe selama periode ini tidak dihitung ke retries, memberi ruang bagi initdb yang kadang lambat.
  6. start_interval: 2s — jeda khusus yang hanya berlaku selama masa hangar, agar status healthy tercapai lebih cepat begitu server siap.

Desain probe yang benar — menguji fungsi layanan, bukan sekadar keberadaan prosesnya — punya babnya sendiri di Chapter Healthcheck dan Service Readiness.

up dengan Urutan yang Sehat #

Budi mendorong perbaikan ini ke repositori, Siti menariknya, lalu menjalankan siklus penuh dari nol:

bash
$ docker compose down
$ docker compose up -d

Output yang Dihasilkan:

text
[+] Running 3/3
 ✔ Network katalog_default   Created
 ✔ Container katalog-db-1    Healthy
 ✔ Container katalog-web-1   Started

Kata pada baris db berubah dari Started menjadi Healthy — dan perubahan satu kata itu adalah seluruh isi janjinya. Mari kita telaah alur keputusan yang barusan terjadi:

Diagram di atas memetakan tanggung jawab pembagian kerja antara Compose dan daemon. Mari kita telaah alurnya:

  1. db dinyalakan lebih dulu — Compose menjalankan service dependensi sebelum service yang bergantung, persis seperti bentuk daftar.
  2. Probe berjalan berulang — selama pg_isready belum lulus, container db berstatus health: starting dan Compose menahan diri untuk tidak melanjutkan.
  3. Gerbang healthy terbuka — begitu probe lulus, Compose baru menciptakan container web; pada mesin Siti, web kini bertahan hidup melewati detik-detik awal yang dulu mematikannya.
  4. Keadaan akhir terlihat di ps — docker compose ps menampilkan db dengan status Up ... (healthy) dan web dengan Up, tanpa satu pun Exited.

restart on-failure: Lapisan Ketahanan #

Gerbang service_healthy menjaga momen start, tetapi hubungan web dan db tidak berakhir di situ. Di tengah jalan db bisa saja dibuat ulang, jaringan bisa tersendat sesaat, dan koneksi yang sudah terbentuk bisa terputus — web akan keluar dengan exit code 1 sama seperti sebelumnya. Kunci restart menambah lapisan ketahanan untuk kejadian semacam itu:

yaml
    restart: on-failure

Kebijakan ini milik keluarga yang kita bedah pada Chapter Restart Policy: no sebagai nilai bawaan, on-failure[:maks], always, dan unless-stopped. Daemon merespons exit code — bukan urusan kesiapan — dengan menyalakan ulang container yang gagal, dan jeda antar percobaan dimulai dari 100 milidetik lalu berlipat ganda.

Tiga tingkat pengaturan urutan start kini bisa dirangkum dalam satu tabel:

TingkatDeklarasi depends_onYang ditunggu sebelum web menyalaKecocokan
1tidak adaTidak ada — Compose bebas memulai paralelService yang saling tidak bergantung
2daftar, atau condition: service_startedContainer db berjalan — prosesnya hidupService yang kesiapannya hampir seketika
3condition: service_healthyContainer db berstatus healthyDatabase dan service berat awal

Urutan start kini berada dalam kendali kita: web tidak lagi berlari sebelum db sanggup menerima koneksi, dan kegagalan yang lolos pun ditangkap restart policy. Namun seluruh bangunan ini bertumpu pada satu benda yang belum pernah kita periksa kelayakannya: probe pg_isready itu sendiri. Healthcheck yang salah rancang membuat gerbang service_healthy bohong besar — meloloskan service yang sakit, atau menggantungkan stack pada service yang sehat. Cara mendesain probe yang jujur, membaca siklus statusnya, dan menyusun berkas katalog final — materi Chapter Healthcheck dan Service Readiness.

Referensi #