Lima service toko kini hidup rukun berdampingan — proxy, frontend, backend, db, dan cache — ditambah satu job migrasi sekali jalan yang ikut menunggu giliran, dan semua nama terselesaikan lewat DNS network Compose persis seperti yang kita bedah di chapter Service Discovery. Namun resolusi nama hanya menjawab “di mana”; bab baru masalahnya menyusul di detik-detik pertama docker compose up: siapa yang boleh menyala lebih dulu. Nama hanya bisa ditemukan bila container di baliknya sudah tercipta, dan yang lebih halus — container yang sudah tercipta belum tentu siap melayani. Stack toko kita adalah rantai kebutuhan berlapis: proxy membutuhkan backend yang benar-benar menjawab, backend membutuhkan db yang sehat, cache yang menjawab PONG, dan skema tabel yang sudah tercipta. Chapter ini merangkai seluruh rantai itu menjadi satu sistem urutan hidup yang eksplisit, lalu membuktikannya baris demi baris pada keluaran docker compose up.
Rantai Kebutuhan yang Berlapis #
Mari kita petakan dulu siapa butuh siapa pada stack yang kita miliki sejauh ini. backend sudah tertolong tiga gerbang sejak chapter Application dan Database dan Cache dan Queue: ia menunggu db berstatus sehat, cache berstatus sehat, dan migrate selesai dengan exit code 0. Yang belum tertangani adalah ujung depannya: proxy masih memakai depends_on bentuk daftar dari chapter Reverse Proxy, sehingga proxy hanya tahu bahwa container backend sudah Started — bukan bahwa backend sudah siap melayani.
Perbedaan Started versus siap inilah peringatan yang kita terima di chapter depends_on dan Startup Order: urutan start, berapa pun ketatnya, tidak menjamin kesiapan aplikasi. nginx membaca nama upstream pada proxy_pass tepat saat prosesnya menyala — bila backend belum terdaftar di DNS, proxy langsung tumbang dengan nginx: [emerg] host not found in upstream; bentuk daftar depends_on menutup celah itu dengan memastikan backend tercipta lebih dulu. Tetapi setelah tercipta, tidak ada satu baris pun di compose.yaml yang memastikan backend sudah membuka pool koneksi dan sanggup menjawab permintaan pertama yang masuk lewat proxy. Rantai kebutuhan lengkapnya terbaca seperti ini:
| Service | Menunggu apa | Sebab |
|---|---|---|
proxy | frontend berjalan, backend sehat | nginx menyelesaikan nama upstream saat start, lalu menunggu backend benar-benar menjawab |
backend | db sehat, cache sehat, migrate tuntas | pool butuh server yang menerima koneksi; pola cache-aside butuh Redis; endpoint butuh skema |
migrate | db sehat | DDL hanya bisa berjalan pada server yang menerima koneksi |
frontend | tidak ada | berkas statis tanpa masa bangun |
db, cache | tidak ada | fondasi rantai — tidak bergantung pada siapa pun |
Baris frontend sengaja kosong: nginx statis yang menyajikan berkas dari bind mount tidak punya masa inisialisasi, sehingga service_started sudah lebih dari cukup untuknya. Kesiapan yang sejati harus dibuktikan, bukan diasumsikan — dan bahasa pembuktianya sudah kita kenal.
Healthcheck sebagai Gerbang Kesiapan #
Chapter Healthcheck dan Service Readiness menetapkan prinsip probe yang jujur: uji fungsi layanan sebagaimana penggunanya mengujinya, bukan sekadar keberadaan prosesnya. Backend toko belum punya healthcheck — hanya db dan cache yang punya — padahal proxy justru butuh membaca kesiapan backend. Kita tambahkan endpoint /healthz yang mengetuk database sungguhan di dalam server.js, di samping endpoint /produk.json yang sudah ada:
if (req.url === "/healthz") {
try {
await pool.query("SELECT 1");
res.writeHead(200).end("ok");
} catch {
res.writeHead(500).end("db tidak siap");
}
return;
}Mari kita telaah cabang baru di atas:
pool.query("SELECT 1")— permintaan termurah yang tetap menempuh jalur lengkap: dari handler, ke pool, ke network, kedb, lalu kembali. Bila salah satu mata rantai itu putus, baris ini melempar galat sebelum jawaban sempat dikirim.try ... catch— kueri yang gagal ditangkap dan dijawab500; tanpa tangkapan, galat tak tertangani justru melumpuhkan seluruh proses Node.200versus500— protokol kejujuran probe: exit codewgetdipetakan langsung dari status HTTP, sehingga “sehat” hanya diberikan bila rantai backend → pool → db benar-benar berfungsi. Backend yang “hidup tapi buta” tidak bisa berpura-pura sehat.
Probe-nya sendiri menempel pada service backend di compose.yaml, memakai wget milik busybox yang hadir di node:22-alpine:
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
start_interval: 5sMari kita telaah kunci-kunci probe tersebut:
testbentukCMD—wget --spider -qmengetuk halaman tanpa mengunduh isinya dan tetap hening; exit code 0 hanya diberikan bila jawabannya 2xx.interval: 10sdantimeout: 3s— pemeriksaan rutin tiap 10 detik dengan batas 3 detik per percobaan, angka yang jauh lebih sehat daripada nilai bawaan 30 detik.start_period: 5sdenganstart_interval: 5s— masa hangar lima detik memberi ruang bagi Node membaca environment dan membuka pool, sementara probe dipercepat menjadi tiap 5 detik selama masa itu agar statushealthytercapai segera setelah listener terbuka.
Rantai depends_on dari Ujung ke Ujung #
Dengan probe backend terpasang, rantai bisa kita lengkapi sampai ujung depan. Berikut compose.yaml toko utuh dengan urutan hidup yang sehat:
name: toko
services:
proxy:
image: nginx:1.29-alpine
ports:
- "8080:80"
configs:
- source: nginx-conf
target: /etc/nginx/conf.d/default.conf
networks:
- frontend-net
- backend-net
depends_on:
frontend:
condition: service_started
backend:
condition: service_healthy
frontend:
image: nginx:1.29-alpine
volumes:
- ./frontend:/usr/share/nginx/html:ro
networks:
- frontend-net
backend:
build: ./api
image: toko-api:dev
environment:
- DATABASE_URL=postgres://toko:rahasia123@db-utama:5432/toko
- REDIS_URL=redis://cache:6379
networks:
- backend-net
depends_on:
db:
condition: service_healthy
cache:
condition: service_healthy
migrate:
condition: service_completed_successfully
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/healthz"]
interval: 10s
timeout: 3s
retries: 3
start_period: 5s
start_interval: 5s
migrate:
build: ./api
image: toko-api:dev
command: ["node", "migrate.js"]
environment:
- DATABASE_URL=postgres://toko:rahasia123@db-utama:5432/toko
networks:
- backend-net
depends_on:
db:
condition: service_healthy
db:
image: postgres:18-alpine
environment:
- POSTGRES_USER=toko
- POSTGRES_PASSWORD=rahasia123
- POSTGRES_DB=toko
volumes:
- db-data:/var/lib/postgresql/data
networks:
backend-net:
aliases:
- db-utama
healthcheck:
test: ["CMD-SHELL", "pg_isready -U toko -d toko"]
interval: 5s
timeout: 3s
retries: 5
start_period: 10s
cache:
image: redis:7-alpine
command: ["redis-server", "--maxmemory", "128mb", "--maxmemory-policy", "allkeys-lru"]
networks:
- backend-net
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
frontend-net:
backend-net:
volumes:
db-data:
configs:
nginx-conf:
file: ./nginx.confMari kita telaah keputusan-keputusan penting pada berkas di atas:
proxymenuntutbackendsehat, bukan sekadar berjalan — gerbangservice_healthykini bermakna penuh karena backend punya probe; proxy baru diciptakan setelah/healthzmenjawab 200, sehingga jendela 502 di menit pertama tertutup sejak awal.frontendtetapservice_started— service statis tanpa masa bangun tidak butuh probe; menuntutservice_healthypadahal healthcheck-nya tak ada justru menggantungkan stack selamanya.backendmenunggu tiga gerbang sekaligus — db dan cache harus sehat, migrate harus tuntas; Compose mengevaluasi ketiganya sebelum container backend diciptakan.- Host
db-utamapadaDATABASE_URL— alias peran yang kita sematkan di chapter *Service Discovery; baik backend maupun migrate memanggil peran "database utama", bukan nama implementasidb`, sehingga suatu hari service pengganti bisa mengambil alih tanpa menyentuh environment keduanya. - Batas dua network tetap terjaga — frontend terisolasi di
frontend-net, sementara backend, db, dan cache hidup dibackend-net; proxy menjadi satu-satunya penjembatan.commandpadacachebeserta batas memori 128 MB dari chapter Cache dan Queue tidak boleh hilang hanya karena urutan start berubah.
Urutan hidup yang dihasilkan berkas tersebut bisa dipetakan utuh:
Diagram di atas memperlihatkan pembagian kerja yang tegas antara Compose dan healthcheck: Compose menciptakan container sesuai relasi depends_on, sedangkan probe-lah yang memutuskan kapan sebuah gerbang terbuka. Dua cabang berjalan paralel — frontend tidak menunggu siapa pun, sementara jalur db → migrate → backend → proxy bergerak bertingkat. Perlu dicatat juga cabang kegagalan pada migrate: exit code selain nol membuat up berhenti dan melaporkan galat, bukan melanjutkan dengan backend yang tanpa skema.
Menyaksikan Urutan Hidup pada Keluaran up #
Teori terbaik tetap perlu dibuktikan. Matikan stack lama bila masih menyala (docker compose down), lalu jalankan dari nol pada volume yang segar:
docker compose up -dOutput yang Dihasilkan:
[+] Running 9/9
✔ Network toko_frontend-net Created 0.1s
✔ Network toko_backend-net Created 0.1s
✔ Volume "toko_db-data" Created 0.0s
✔ Container toko-cache-1 Healthy 4.1s
✔ Container toko-db-1 Healthy 12.2s
✔ Container toko-migrate-1 Exited 2.4s
✔ Container toko-backend-1 Started 0.6s
✔ Container toko-frontend-1 Started 0.4s
✔ Container toko-proxy-1 Started 6.8sSembilan baris itu adalah kronik urutan hidup yang kita rancang. Mari kita telaah peristiwanya satu per satu:
- Dua network dan satu volume diciptakan lebih dulu — fondasi tempat para service akan berdiri; pool subnet dan volume kita kenal sejak section jaringan dan penyimpanan data.
cachesehat dalam 4,1 detik,dbdalam 12,2 detik — keduanya dinyalakan bersamaan, tetapi PostgreSQL membayar waktu lebih lama untukinitdbpada volume yang masih kosong; angka 12,2 detik itu persis jendela race yang dulu mematikan aplikasi katalog di mesin Siti.migratebaru berjalan setelahdbsehat — bukan sekadar setelah dinyalakan; DDL menunggu server benar-benar menerima koneksi, lalu job selesai dalam 2,4 detik dan keluar.backendmenyala setelah tiga gerbang terbuka — db sehat, cache sehat, danmigratetelahExiteddengan exit code 0; barisnya tercatatStartedkarena memang begitulah status akhirnya.proxypaling akhir dengan 6,8 detik — jeda setelah backendStartedadalah masa probe/healthz: proxy baru diciptakan setelah backend menjawab 200. Setelah baris terakhir, satu permintaan pertama kehttp://localhost:8080/api/produk.jsonpun langsung dilayani tanpa jendela 502.
Keadaan akhir stack terbaca dari docker compose ps -a:
docker compose ps -aOutput yang Dihasilkan:
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
toko-proxy-1 nginx:1.29-alpine "/docker-entrypoint.…" proxy 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp
toko-frontend-1 nginx:1.29-alpine "/docker-entrypoint.…" frontend 2 minutes ago Up 2 minutes
toko-backend-1 toko-api:dev "docker-entrypoint.s…" backend 2 minutes ago Up 2 minutes (healthy)
toko-db-1 postgres:18-alpine "docker-entrypoint.s…" db 2 minutes ago Up 2 minutes (healthy)
toko-cache-1 redis:7-alpine "docker-entrypoint.s…" cache 2 minutes ago Up 2 minutes (healthy)
toko-migrate-1 toko-api:dev "docker-entrypoint.s…" migrate 2 minutes ago Exited (0) 2 minutes agoMigrasi One-Shot: Mati yang Sehat #
Baris terakhir tabel itu sering mengejutkan pendatang baru: toko-migrate-1 berstatus Exited (0) di tengah stack yang semuanya Up. Status itu bukan kegagalan — justru itulah definisi sukses bagi service one-shot. Mari kita telaah mengapa “mati” adalah kabar baik untuk job migrasi:
- Exit code 0 adalah laporan tugas selesai —
migrate.jsmenjalankanCREATE TABLE IF NOT EXISTShingga tuntas lalu keluar dengan sengaja; container yang tetap hidup justru patut dicurigai sedang menggantung di tengah DDL. - Compose telah memverifikasi kematiannya — gerbang
service_completed_successfullymembaca exit code container; hanya angka 0 yang membuka jalan bagi backend.Exited (0)padaps -aadalah bukti verifikasi yang sama. - Kebijakan restart bawaan
nomenjaga kehormatan job — daemon tidak membangkitkan ulang container yang selesai dengan sukses, jadi job tidak diulang sia-sia.
Dua Replika, Satu Migrasi #
Sisi paling tajam dari pola one-shot terlihat saat backend direplikasi. Ingat kembali dua strategi migrasi dari chapter Application dan Database: menjalankan migrasi di entrypoint aplikasi, atau menjalankannya sebagai service tersendiri. Strategi pertama tampak sederhana dan cukup untuk satu replika — tetapi coba bayangkan --scale backend=3 dengan migrasi di entrypoint: tiga container menyala hampir bersamaan, dan ketiganya menjalankan migrasi yang sama di jendela waktu yang hampir sama. Mari kita telaah mengapa itu resep bencana:
- DDL berlomba tiga arah — dua replika bisa sama-sama melewati pemeriksaan “tabel masih kosong” lalu sama-sama memasukkan data awal; salah satunya menabrak
duplicate key value violates unique constraint "produk_pkey"dan gagal start padahal aplikasinya sehat. - Perubahan skema mengunci tabel —
ALTER TABLEpada migrator pertama membuat migrator kedua menunggu; pada susunan yang tersarik, keduanya saling menunggu dan PostgreSQL mendeteksi deadlock. Kegagalan semacam ini hanya muncul saat scaling, sulit direproduksi, dan selalu datang pada jam tersibuk. - One-shot memutus persoalan jumlah secara struktural — satu service
migrateberarti satu container, satu migrator, titik. Gerbangservice_completed_successfullymemastikan migrasi tunggal itu tuntas sebelum replika backend pertama menyala; skala backend berapa pun, migrasinya tetap satu.
Bukti terakhirnya bisa kita saksikan langsung pada stack yang sedang berjalan:
docker compose up -d --scale backend=3Output yang Dihasilkan:
[+] Running 2/2
✔ Container toko-backend-2 Started 0.5s
✔ Container toko-backend-3 Started 0.6sTidak ada satu baris migrate pada keluaran itu: gerbang replika baru dievaluasi terhadap container migrate yang sudah Exited (0), sehingga kedua replika langsung menyusul tanpa migrasi ulang. Seperti di chapter Application dan Database, proxy perlu membaca ulang daftar alamatnya (docker compose restart proxy) agar nginx mengenal ketiga backend, lalu kembalikan stack ke satu replika dengan docker compose up -d --scale backend=1 sebelum melanjutkan.
Gerbang Bukan Janji Seumur Hidup #
Rantai yang kita bangun menjaga satu momen: up. Selepas momen itu, depends_on tidak lagi bisa berbuat apa-apa — bila db dibuat ulang di siang hari yang ramai, koneksi pool yang terbuka tetap terputus dan backend tetap merasakan kegagalan. Persis di titik inilah retry dengan backoff eksponensial dari chapter Service Discovery mengambil alih: aplikasi mencoba ulang dengan jeda yang membesar, memberi sistem yang sedang bangkit ruang bernapas tanpa menyerah pada ECONNREFUSED pertama. Lapis pemulihan restart: on-failure dari chapter Restart Policy menutup celah terakhir untuk kegagalan yang membuat proses keluar sama sekali. Gerbang, retry, dan restart bukan pilihan salah satu — keduanya bekerja pada waktu yang berbeda dan saling menutup celah.
Tiga Tingkat Pola Dependency #
Seluruh perjalanan urutan start dari section Docker Compose sampai chapter ini kini bisa dirangkum dalam satu tangga pola:
| Tingkat | Deklarasi depends_on | Yang ditunggu sebelum service menyala | Cocok untuk |
|---|---|---|---|
| 1 | daftar, atau condition: service_started | Container dependensi berjalan — prosesnya hidup | Service ringan tanpa masa bangun, seperti frontend statis |
| 2 | condition: service_healthy | Healthcheck dependensi lulus — layanannya menjawab | db, cache, dan aplikasi dengan masa inisialisasi |
| 3 | condition: service_completed_successfully | Container dependensi keluar dengan exit code 0 | Job one-shot: migrate hari ini, seed di chapter berikutnya |
Tangga ini adalah alat keputusan, bukan aturan kekakuan: naiklah satu tingkat hanya bila tingkat di bawahnya terbukti tidak cukup. Stack toko kini menyala dengan urutan yang teratur dan terbaca — setiap baris keluaran up punya penjelasan, setiap gerbang punya probe yang jujur. Namun siklus hidup punya dua ujung, dan ujung yang satu belum kita rapikan: cara stack berhenti. docker compose stop mengirim SIGTERM ke PID 1 setiap container, dan aplikasi yang tidak menangkapnya akan mati dengan koneksi pool terpotong serta permintaan setengah jadi di tangan pelanggan. Cara berhenti dengan sopan adalah materi chapter Graceful Shutdown.
Referensi #
- Control startup and shutdown order in Compose
— pola
depends_onber kondisi dan service one-shot langsung dari dokumentasi resmi. - Compose File Reference — Services
— definisi
depends_on,healthcheck, dan kunci service lainnya. - docker compose up — CLI Reference
— perilaku
upterhadap gerbang kesiapan dan status container. - PostgreSQL
— image resmi beserta utilitas
pg_isreadyuntuk probe kesiapan.