Build CoreOrdered learning track

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.

12 min read2321 words
PrevNext
Lesson 1735 lesson track0719 Build Core
#docker#containerization#docker-compose#healthcheck+2 more

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_on lalu 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.

LevelPertanyaanMekanismeContoh
Container orderService dibuat sebelum service lain?depends_ondb dibuat sebelum api
Runtime readinessDependency sehat sebelum dependent dibuat?healthcheck + condition: service_healthyapi menunggu db healthy
Application resilienceAplikasi tetap benar saat dependency restart/hilang?retry, timeout, circuit breaker, reconnectAPI 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:

  1. Long-running dependency: api membutuhkan postgres, redis, dan mailpit tetap berjalan.
  2. One-shot dependency: api membutuhkan migration selesai 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 postgres dan redis sebelum api.
  • Compose menghentikan api sebelum postgres dan redis.
  • 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.

ConditionMakna praktisCocok untuk
service_starteddependency container sudah startRedis/dev helper cepat, service dengan retry kuat
service_healthydependency punya healthcheck dan status healthydatabase, broker, search engine, emulator eksternal
service_completed_successfullydependency adalah job yang harus exit code 0migrasi 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 checkPertanyaanContohDampak salah desain
Startup checkApakah proses sudah melewati fase boot?JVM selesai warmup, DB recovery selesaidependent start terlalu cepat
Readiness checkApakah service siap menerima request bermakna?/ready mengecek DB migration versiontraffic masuk saat service belum siap
Liveness checkApakah service masih hidup dan tidak deadlock?/live hanya cek event loop/processrestart 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

FieldMaknaGuideline
testcommand yang menentukan sehat/tidakgunakan command murah dan spesifik
intervaljarak antar checkjangan terlalu rapat untuk DB berat
timeoutbatas waktu satu checkharus lebih kecil dari interval
retriesjumlah kegagalan sebelum unhealthysesuaikan dengan cold start
start_periodgrace period awalpenting 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:

  1. Compose membuat postgres.
  2. Compose menjalankan healthcheck postgres.
  3. Compose menunggu postgres healthy.
  4. Compose membuat api.
  5. 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:

  • healthy bukan berarti seluruh dependency downstream sehat.
  • unhealthy tidak 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:

  1. Tambahkan binary minimal yang diperlukan.
  2. Pakai healthcheck command native aplikasi.
  3. Buat endpoint dan gunakan runtime base image yang memang mendukung check.
  4. 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.

EnvironmentRestart policy guideline
local devdefault/no restart agar error terlihat
integration testbiasanya no restart agar test fail cepat
demo envunless-stopped bisa membantu
production single-hosthati-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 /ready yang 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 localhost bukan service DNS postgres;
  • 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:

  1. Apakah service ini long-running atau one-shot job?
  2. Dependency mana yang mandatory saat startup?
  3. Dependency mana yang bisa retry setelah startup?
  4. Apakah dependency punya healthcheck semantic?
  5. Apakah dependent harus menunggu service_started, service_healthy, atau service_completed_successfully?
  6. Apakah migration/seed job idempotent?
  7. Apakah startup graph acyclic?
  8. Apakah shutdown order memberi waktu graceful termination?
  9. Apakah failure terlihat jelas dari logs dan exit code?
  10. 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:

  • api start cepat;
  • postgres butuh beberapa detik sampai ready;
  • tanpa condition: service_healthy, API gagal.

Tugas:

  1. Tambahkan healthcheck ke Postgres.
  2. Tambahkan depends_on.postgres.condition: service_healthy.
  3. Tambahkan retry bounded di API.
  4. 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

LevelIndikator
Juniormemakai depends_on short syntax dan sleep
Intermediatememakai healthcheck untuk DB dan broker
Seniormemisahkan 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.

Lesson Recap

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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.