Compose Dependency Design: Healthchecks, Startup Order, Shutdown Order
Learn Docker, Containerization, Docker Compose, Docker Swarm - Part 017
Deep dive into Docker Compose dependency design, healthchecks, startup order, shutdown order, service readiness, migration jobs, restart coupling, and failure-safe multi-service bootstrapping.
Part 017 — Compose Dependency Design: Healthchecks, Startup Order, Shutdown Order
Target pembelajaran: setelah part ini, kita mampu mendesain startup dan shutdown multi-service Compose stack secara deterministik, aman, dan debuggable; bukan sekadar menambah
depends_onlalu berharap dependency sudah siap.
Part 015 membahas Compose sebagai application graph. Part 016 membahas Compose file semantics. Part ini fokus pada salah satu failure mode paling umum dalam sistem multi-container: service sudah running, tetapi belum ready.
Docker Compose dapat mengatur urutan startup dan shutdown dengan depends_on. Namun, Compose secara default hanya tahu bahwa container sudah dibuat atau berjalan. Untuk readiness yang lebih kuat, kita perlu menggabungkan depends_on.condition, healthcheck, retry di aplikasi, dan desain dependency graph yang tidak rapuh.
1. Mental Model: Startup Bukan Sekadar Urutan Container
Banyak engineer memulai dengan asumsi ini:
postgres start dulu -> api start -> aplikasi aman
Asumsi itu lemah.
Database bisa berada dalam beberapa fase:
container created
container running
postgres process started
port 5432 listening
WAL recovery selesai
database menerima query
schema/migration tersedia
connection pool stabil
Dari perspektif API, hanya fase terakhir yang benar-benar berguna.
Compose dapat mengurutkan container, tetapi readiness aplikasi adalah kontrak semantic yang harus kita definisikan.
2. Tiga Level Dependency
Dalam Compose, dependency bisa dilihat dalam tiga level.
| Level | Pertanyaan | Mekanisme | Contoh |
|---|---|---|---|
| Container order | Service dibuat sebelum service lain? | depends_on | db dibuat sebelum api |
| Runtime readiness | Dependency sehat sebelum dependent dibuat? | healthcheck + condition: service_healthy | api menunggu db healthy |
| Application resilience | Aplikasi tetap benar saat dependency restart/hilang? | retry, timeout, circuit breaker, reconnect | API reconnect ke DB setelah DB restart |
Top engineer tidak berhenti di level pertama. Compose dependency yang baik hanya mengurangi race, bukan menggantikan resilience aplikasi.
3. Compose Dependency Graph
Compose project idealnya dapat dibaca sebagai graph.
Graph di atas memiliki dua jenis dependency:
- Long-running dependency:
apimembutuhkanpostgres,redis, danmailpittetap berjalan. - One-shot dependency:
apimembutuhkanmigrationselesai sukses sebelum menerima traffic.
Keduanya tidak boleh dimodelkan dengan cara yang sama.
4. depends_on: Short Syntax vs Long Syntax
4.1 Short Syntax
services:
api:
build: .
depends_on:
- postgres
- redis
postgres:
image: postgres:16-alpine
redis:
image: redis:7-alpine
Short syntax berarti:
- Compose membuat
postgresdanredissebelumapi. - Compose menghentikan
apisebelumpostgresdanredis. - Compose tidak otomatis menunggu database siap menerima query.
Ini cukup untuk dependency yang sangat cepat atau service yang sudah punya retry kuat. Namun untuk database, broker, search engine, atau service lambat, short syntax sering tidak cukup.
4.2 Long Syntax
services:
api:
build: .
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
Long syntax memungkinkan kita mengekspresikan kondisi.
| Condition | Makna praktis | Cocok untuk |
|---|---|---|
service_started | dependency container sudah start | Redis/dev helper cepat, service dengan retry kuat |
service_healthy | dependency punya healthcheck dan status healthy | database, broker, search engine, emulator eksternal |
service_completed_successfully | dependency adalah job yang harus exit code 0 | migrasi DB, schema setup, seed data, init bucket |
5. Readiness vs Liveness vs Startup Check
Healthcheck sering disalahgunakan karena semua hal disebut “health”. Untuk desain yang benar, bedakan tiga konsep.
| Jenis check | Pertanyaan | Contoh | Dampak salah desain |
|---|---|---|---|
| Startup check | Apakah proses sudah melewati fase boot? | JVM selesai warmup, DB recovery selesai | dependent start terlalu cepat |
| Readiness check | Apakah service siap menerima request bermakna? | /ready mengecek DB migration version | traffic masuk saat service belum siap |
| Liveness check | Apakah service masih hidup dan tidak deadlock? | /live hanya cek event loop/process | restart loop karena dependency eksternal lambat |
Dalam Compose, healthcheck digunakan sebagai sinyal status container. Untuk local/dev/test, healthcheck readiness sering praktis. Untuk production orchestrator, biasanya readiness dan liveness dipisah lebih eksplisit.
6. Anatomy of healthcheck
Contoh Postgres:
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Perhatikan $$.
Dalam Compose file, $VAR bisa diinterpolasi oleh Compose dari host environment. Jika kita ingin variable dievaluasi di dalam container oleh shell, gunakan $$VAR.
6.1 Field Penting
| Field | Makna | Guideline |
|---|---|---|
test | command yang menentukan sehat/tidak | gunakan command murah dan spesifik |
interval | jarak antar check | jangan terlalu rapat untuk DB berat |
timeout | batas waktu satu check | harus lebih kecil dari interval |
retries | jumlah kegagalan sebelum unhealthy | sesuaikan dengan cold start |
start_period | grace period awal | penting untuk DB, JVM, search engine |
6.2 CMD vs CMD-SHELL
healthcheck:
test: ["CMD", "redis-cli", "ping"]
Gunakan CMD jika tidak perlu shell expansion, pipe, &&, atau variable shell.
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
Gunakan CMD-SHELL jika butuh shell behavior.
6.3 Exit Code Contract
Healthcheck command harus mengikuti kontrak sederhana:
exit code 0 -> healthy
non-zero exit -> unhealthy
Jangan menulis healthcheck yang hanya mencetak output tetapi selalu exit 0.
7. Pattern 1: API Menunggu Database Healthy
services:
api:
build:
context: .
environment:
DATABASE_URL: postgres://app:app@postgres:5432/app
depends_on:
postgres:
condition: service_healthy
ports:
- "127.0.0.1:8080:8080"
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
volumes:
postgres-data: {}
Behavior:
- Compose membuat
postgres. - Compose menjalankan healthcheck
postgres. - Compose menunggu
postgreshealthy. - Compose membuat
api. - API tetap harus punya retry karena dependency bisa gagal setelah startup.
7.1 Kenapa Retry Tetap Wajib?
depends_on.condition: service_healthy hanya mengatur startup order. Ia tidak menjamin dependency akan selalu tersedia setelah dependent berjalan.
Skenario nyata:
- database restart setelah API sudah running;
- network glitch di Docker Desktop;
- volume corrupted;
- schema migration lock;
- connection pool stale;
- Redis flush/restart;
- service external emulator mati.
Readiness order mengurangi race saat boot. Resilience runtime tetap tanggung jawab aplikasi.
8. Pattern 2: Migration Job Sebelum API
Untuk database-backed service, masalah bukan hanya database ready. Schema juga harus berada di versi yang benar.
Compose model:
services:
api:
build: .
environment:
DATABASE_URL: postgres://app:app@postgres:5432/app
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
ports:
- "127.0.0.1:8080:8080"
migrate:
build: .
command: ["./bin/migrate", "up"]
environment:
DATABASE_URL: postgres://app:app@postgres:5432/app
depends_on:
postgres:
condition: service_healthy
restart: "no"
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 20s
Important invariant:
API boleh start hanya jika migration job exit 0.
Ini jauh lebih kuat daripada API start lalu mencoba migrate sendiri secara implicit.
8.1 Kapan Migration Job Buruk?
Migration job dalam Compose bagus untuk local dev dan integration test. Untuk production, migration perlu governance lebih kuat:
- backward-compatible migration;
- locking behavior;
- rollback strategy;
- audit log;
- dry-run;
- migration owner;
- deployment ordering;
- blast radius control.
Compose membantu menjalankan urutan. Compose tidak menggantikan database release governance.
9. Pattern 3: Seed Data sebagai One-Shot Job
Untuk local development, seed data sering diperlukan.
services:
api:
build: .
depends_on:
seed:
condition: service_completed_successfully
seed:
build: .
command: ["./bin/seed-dev-data"]
environment:
DATABASE_URL: postgres://app:app@postgres:5432/app
depends_on:
migrate:
condition: service_completed_successfully
restart: "no"
migrate:
build: .
command: ["./bin/migrate", "up"]
depends_on:
postgres:
condition: service_healthy
restart: "no"
Namun seed data harus idempotent.
Bad seed:
INSERT INTO users(email) VALUES ('admin@example.test');
Better seed:
INSERT INTO users(email)
VALUES ('admin@example.test')
ON CONFLICT (email) DO NOTHING;
Idempotency membuat docker compose up tidak berubah menjadi operasi destruktif.
10. Pattern 4: Explicit Restart Coupling
Compose long syntax mendukung restart: true di dalam depends_on untuk kasus explicit Compose operation.
services:
api:
build: .
depends_on:
postgres:
condition: service_healthy
restart: true
Makna praktis:
- Jika dependency di-restart oleh operasi Compose eksplisit, dependent juga dapat di-restart.
- Ini membantu service yang tidak otomatis reconnect dengan baik.
- Ini bukan pengganti reconnect logic yang benar di aplikasi.
Gunakan secara selektif.
Jika semua service saling restart, development stack menjadi noisy dan sulit di-debug.
11. Shutdown Order
Startup order sering diperhatikan. Shutdown order sering dilupakan.
Compose menghentikan service dalam dependency order terbalik.
Jika api depends on postgres, maka saat docker compose down:
api stop dulu
postgres stop setelahnya
Ini masuk akal karena API masih butuh DB saat graceful shutdown:
- menyelesaikan request aktif;
- flush telemetry;
- menyimpan offset;
- menutup transaction;
- drain queue consumer.
11.1 stop_grace_period
Jika aplikasi butuh waktu shutdown lebih panjang:
services:
api:
build: .
stop_grace_period: 45s
Tanpa grace period yang cukup, Compose dapat mengirim signal terminasi lanjutan sebelum aplikasi selesai cleanup.
11.2 Signal Handling Tetap Penting
Compose tidak bisa memperbaiki aplikasi yang mengabaikan signal.
Aplikasi harus:
- menerima
SIGTERM; - berhenti menerima request baru;
- menyelesaikan request aktif;
- menutup connection pool;
- flush metrics/logs jika perlu;
- exit dengan code yang benar.
12. Compose Health State Machine
Container dengan healthcheck memiliki state tambahan.
Penting:
healthybukan berarti seluruh dependency downstream sehat.unhealthytidak selalu berarti container otomatis restart oleh Compose.- healthcheck harus murah, deterministik, dan merepresentasikan readiness service tersebut.
13. Designing Good Healthchecks
13.1 Database
Good:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
Better jika schema readiness penting:
healthcheck:
test:
[
"CMD-SHELL",
"psql -U $${POSTGRES_USER} -d $${POSTGRES_DB} -tAc \"select 1 from schema_version where version >= 42\" | grep -q 1"
]
Namun hati-hati: healthcheck yang terlalu berat dapat membebani DB.
13.2 Redis
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
13.3 HTTP API
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/ready || exit 1"]
interval: 10s
timeout: 3s
retries: 6
start_period: 20s
Namun image minimal sering tidak punya curl atau wget. Pilihan:
- Tambahkan binary minimal yang diperlukan.
- Pakai healthcheck command native aplikasi.
- Buat endpoint dan gunakan runtime base image yang memang mendukung check.
- Hindari memasukkan terlalu banyak tool hanya demi healthcheck jika image production harus minimal.
13.4 Queue Consumer
Queue consumer tidak selalu punya HTTP port. Healthcheck bisa berupa:
- proses masih hidup;
- koneksi broker aktif;
- consumer group registered;
- lag masih di bawah threshold untuk environment tertentu.
Untuk Compose local, sering cukup:
healthcheck:
test: ["CMD-SHELL", "test -f /tmp/consumer-ready"]
Aplikasi membuat file readiness setelah koneksi broker terbentuk.
14. App-Level Retry: Dependency Order Tidak Cukup
Aplikasi yang benar harus punya retry boundary.
Contoh pseudo-flow:
on startup:
load config
create connection pool with bounded retry
run readiness validation
expose /ready only after dependency checks pass
on runtime dependency failure:
mark /ready false
keep /live true if process is healthy
retry with backoff
avoid unbounded request blocking
Retry yang buruk:
while true:
connect()
Retry yang baik:
retry with exponential backoff + jitter
bounded startup deadline
clear error logs
readiness false until dependency available
14.1 Failure Mode: Infinite Boot Hang
Jika service hang selamanya saat dependency tidak tersedia, Compose terlihat seperti stuck.
Better behavior:
- startup retry sampai batas tertentu;
- log alasan spesifik;
- exit non-zero jika dependency mandatory tidak tersedia;
- biarkan restart policy atau operator melihat failure.
15. Restart Policy vs Dependency Readiness
Compose service bisa punya restart policy.
services:
api:
restart: unless-stopped
Untuk local development, sering lebih baik tidak memakai restart policy agresif agar failure terlihat.
| Environment | Restart policy guideline |
|---|---|
| local dev | default/no restart agar error terlihat |
| integration test | biasanya no restart agar test fail cepat |
| demo env | unless-stopped bisa membantu |
| production single-host | hati-hati, tetap butuh observability |
Restart policy tidak menyelesaikan root cause. Ia hanya mengulang lifecycle.
16. Avoiding Dependency Cycles
Bad graph:
Contoh nyata:
- API menunggu worker ready.
- Worker menunggu API ready.
- Keduanya tidak pernah ready.
Solusi:
- pecahkan dependency dengan broker;
- jadikan satu dependency optional;
- hilangkan startup dependency, gunakan runtime retry;
- buat readiness tidak bergantung pada peer yang bergantung balik.
Rule of thumb:
Startup dependency graph harus DAG.
DAG = Directed Acyclic Graph.
17. Ports Are Not Readiness
Port terbuka tidak berarti service siap.
Bad healthcheck:
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 5432"]
Masalah:
- process bisa listen tetapi belum menerima query;
- DB bisa recovery;
- API bisa bind port sebelum dependency siap;
- broker bisa menerima TCP tetapi belum siap exchange/topic.
Better:
- gunakan command native (
pg_isready,redis-cli ping); - gunakan endpoint
/readyyang memvalidasi dependency mandatory; - gunakan query ringan yang benar-benar memverifikasi semantic readiness.
18. Sleep Is a Smell
Bad:
services:
api:
command: ["sh", "-c", "sleep 20 && ./start-api"]
Kenapa buruk:
- terlalu pendek di mesin lambat;
- terlalu panjang di mesin cepat;
- tidak memberi sinyal error;
- memperlambat feedback loop;
- menyembunyikan readiness contract.
Sleep dapat diterima hanya sebagai mitigation sementara saat debugging, bukan desain final.
19. Compose Dependency for Test Environments
Untuk integration test, Compose dapat menjalankan dependency lalu test runner.
services:
test:
build:
context: .
target: test
command: ["./gradlew", "integrationTest"]
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
environment:
DATABASE_URL: postgres://app:app@postgres:5432/app
REDIS_URL: redis://redis:6379
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 20
redis:
image: redis:7-alpine
Test service sebaiknya one-shot dan exit code-nya menjadi hasil pipeline.
Jalankan:
docker compose up --build --abort-on-container-exit --exit-code-from test test
Lalu cleanup:
docker compose down -v --remove-orphans
20. Debugging Startup Order
20.1 Lihat Status Service
docker compose ps
Perhatikan status seperti:
Up 15 seconds (health: starting)
Up 30 seconds (healthy)
Up 2 minutes (unhealthy)
Exited (1)
20.2 Lihat Logs Semua Service
docker compose logs -f
Atau service spesifik:
docker compose logs -f postgres api
20.3 Inspect Health Detail
docker inspect <container-name> --format '{{json .State.Health}}'
Gunakan untuk melihat output healthcheck terakhir.
20.4 Jalankan Healthcheck Manual
docker compose exec postgres pg_isready -U app -d app
Jika command healthcheck gagal saat dijalankan manual, perbaiki command sebelum menyalahkan Compose.
21. Common Failure Cases
21.1 Healthcheck Command Tidak Ada
/ bin / sh: curl: not found
Penyebab:
- image minimal tidak punya
curl; - command mengasumsikan shell tertentu;
- path binary berbeda.
Solusi:
- gunakan command yang tersedia;
- tambahkan tool minimal di dev image;
- buat executable healthcheck di aplikasi.
21.2 Variable Diinterpolasi Terlalu Awal
Bad:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
Jika ${POSTGRES_USER} tidak ada di host, Compose bisa menginterpolasi menjadi kosong.
Better:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER}"]
21.3 Dependency Healthy Tetapi API Tetap Gagal
Kemungkinan:
- schema belum migrate;
- wrong database name;
- wrong credentials;
- API memakai
localhostbukan service DNSpostgres; - network berbeda;
- healthcheck dependency terlalu dangkal;
- app tidak punya retry.
21.4 Healthcheck Terlalu Berat
Healthcheck yang melakukan query kompleks tiap 1 detik dapat merusak environment sendiri.
Guideline:
Healthcheck harus cukup kuat untuk readiness, tetapi cukup murah untuk dijalankan terus-menerus.
22. Designing /live and /ready Endpoints
Untuk HTTP service, endpoint yang baik:
GET /live
- return 200 jika process/event loop masih hidup
- tidak query dependency eksternal berat
GET /ready
- return 200 jika service siap menerima traffic
- cek dependency mandatory
- cek migration/schema version jika relevan
- timeout cepat
Bad /live:
/live checks database, redis, downstream API, object storage, and payment gateway
Jika DB lambat, orchestrator bisa restart service padahal process sehat. Itu memperparah outage.
Better:
/live: process health
/ready: serving readiness
Compose sering menggunakan /ready sebagai healthcheck untuk local/test.
23. Compose Dependency Design Checklist
Untuk setiap service, jawab:
- Apakah service ini long-running atau one-shot job?
- Dependency mana yang mandatory saat startup?
- Dependency mana yang bisa retry setelah startup?
- Apakah dependency punya healthcheck semantic?
- Apakah dependent harus menunggu
service_started,service_healthy, atauservice_completed_successfully? - Apakah migration/seed job idempotent?
- Apakah startup graph acyclic?
- Apakah shutdown order memberi waktu graceful termination?
- Apakah failure terlihat jelas dari logs dan exit code?
- Apakah local dev tetap cepat?
24. Production-Level Thinking: Compose vs Orchestrator
Compose dependency design bagus untuk:
- local development;
- integration testing;
- demos;
- single-host utility stack;
- reproducible onboarding;
- ephemeral environment.
Namun, jangan mengira Compose dependency sama dengan production traffic management.
Production-grade orchestrator biasanya butuh:
- readiness gate sebelum traffic route;
- liveness restart policy;
- rolling update;
- service discovery;
- secret management;
- scheduling;
- health-based replacement;
- observability;
- autoscaling atau capacity policy.
Compose membuat local/system test graph lebih deterministik. Ia bukan pengganti seluruh platform runtime.
25. Practice Lab
Lab 1 — Fix Startup Race
Buat Compose stack:
apistart cepat;postgresbutuh beberapa detik sampai ready;- tanpa
condition: service_healthy, API gagal.
Tugas:
- Tambahkan healthcheck ke Postgres.
- Tambahkan
depends_on.postgres.condition: service_healthy. - Tambahkan retry bounded di API.
- Buktikan dengan
docker compose logs -f.
Lab 2 — Add Migration Job
Tambahkan service migrate yang:
- menunggu Postgres healthy;
- menjalankan migration;
- exit 0 saat sukses;
- membuat API menunggu
service_completed_successfully.
Lab 3 — Break and Debug
Sengaja rusak:
- password DB salah;
- healthcheck command salah;
- API memakai
localhost; - migration exit 1.
Untuk masing-masing, tulis:
symptom -> command observasi -> root cause -> fix -> prevention
26. Review Rubric
| Level | Indikator |
|---|---|
| Junior | memakai depends_on short syntax dan sleep |
| Intermediate | memakai healthcheck untuk DB dan broker |
| Senior | memisahkan startup order, readiness, retry, dan migration job |
| Staff+ | mendesain dependency graph sebagai invariant, failure mode, dan operational contract |
27. Ringkasan
Compose dependency design yang baik memiliki invariant berikut:
Container order tidak sama dengan readiness.
Readiness harus diekspresikan dengan healthcheck atau job completion.
Aplikasi tetap harus tahan dependency failure setelah startup.
Startup graph harus acyclic.
Shutdown order sama pentingnya dengan startup order.
Sleep bukan readiness strategy.
Part berikutnya membahas Compose development workflows: inner loop, hot reload, database lokal, queue, seed data, emulator eksternal, dan bagaimana membuat environment lokal yang cepat tetapi tetap mirip production.
You just completed lesson 17 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.