Docker Compose: Healthcheck, depends_on, Profile, dan Build Multi-Stage
depends_on terlihat seperti jaminan bahwa database siap sebelum aplikasi jalan, padahal Compose hanya memastikan container-nya sudah dimulai. Artikel ini membedah perbedaan itu memakai Compose v5.5.1 di mesin uji, termasuk tiga kesalahan yang jarang dibaca tapi paling sering membuat pemula tertahan: dependency tanpa healthcheck, migrasi yang dijalankan ulang, dan profiled service yang dipakai sebagai dependency.
Ringkasan
depends_ontanpaconditionhanya menunggu container dependent benar-benar running.service_healthymembuat Compose menunggu healthcheck dependency benar-benar lolos.- Dependency tanpa blok
healthcheckmembuatservice_healthygagal dengan error, bukan menunggu. service_completed_successfullydipakai untuk container sekali jalan seperti migrasi.restart: trueme-restart service dependent setiap kali dependency di-update.profilesmenyembunyikan service tambahan sampai profilnya diaktifkan.--waitdan--wait-timeoutmenunggu proyek healthy dengan batas waktu jelas.- Build multi-stage memangkas image Go 364 MB menjadi 21,5 MB dalam tiga layer.
Lingkungan Pengujian
Seluruh output di bawah berasal dari mesin uji, bukan dari dokumentasi semata.
| Komponen | Nilai Teruji | Keterangan |
|---|---|---|
| Docker Engine | 29.8.1 | Klien dan daemon versi sama |
| Docker Compose | v5.5.1 | Plugin docker-compose-plugin |
| Image uji | postgres:17, alpine:latest 13 MB, golang:1.26-alpine 364 MB | Postgres 17, Alpine 3.24.2 |
| Waktu build multi-stage | 11 detik (cold) | Docker BuildKit bawaan |
Field
healthcheckdi Compose mengikuti aturan dan nilai default yang sama dengan instruksiHEALTHCHECKdi Dockerfile, dan Compose bisa menimpanya. Lihat referensiHEALTHCHECKuntuk detail sisi Dockerfile.
Masalah Nyata: Dependency yang Hanya Diberntai
Dua container dengan depends_on bentuk pendek dianggap aman oleh banyak orang:
services:
api:
image: alpine:latest
command: sh -c 'sleep 300'
depends_on:
- db
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: demoUrutan yang dijamin Compose hanyalah “container db dimulai sebelum api dimulai”. Postgres yang baru menyala butuh beberapa detik sebelum bisa menerima koneksi, dan api sudah berjalan jauh sebelum itu. Forma pendek depends_on setara dengan condition: service_started, seperti yang ditulis Compose Specification.
Menulis Healthcheck yang Berguna
Healthcheck adalah perintah yang dijalankan berulang di dalam container untuk menentukan apakah layanannya masih bisa dipakai. Bentuk referensinya:
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: demo
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 10
start_period: 3s| Field | Arti | Catatan Teruji |
|---|---|---|
test | Perintah cek | Bentuk list wajib diawali NONE, CMD, atau CMD-SHELL |
interval | Jarak antar pemeriksaan | Duration, bukan detik polos |
timeout | Batas waktu satu pemeriksaan | Keluarannya bukan 0 dihitung sebagai gagal |
retries | Jumlah kegagalan berturut-turut sebelum unhealthy | Dipakai saat condition: service_healthy |
start_period | Masa tenggang saat start | Kegagalan di masa ini tidak dihitung sebagai unhealthy |
start_interval | Interval khusus selama start_period | Bawaan: interval bila tidak diisi |
test boleh berupa string; menurut spesifikasi Compose itu setara dengan CMD-SHELL memakai shell default container (/bin/sh untuk Linux). Bentuk CMD lebih aman untuk image yang tidak punya shell.
Status health terlihat langsung di docker compose ps:
$ docker compose ps
slow Up 1 second (health: starting)
$ docker compose ps
slow Up 9 seconds (healthy)Penting memilih perintah cek yang murah. wget -qO- http://127.0.0.1:8080/healthz untuk aplikasi web, pg_isready untuk database, redis-cli ping untuk cache. Cek database penuh per beberapa detik bisa memperlambat database itu sendiri, jadi sediakan endpoint /healthz yang ringan bila aplikasinya memang bisa.
Condition Depends_on: Tiga Cara Menunggu
Bentuk panjang depends_on menambah tiga field: condition, restart, dan required.
condition | Compose menunggu sampai | Kasus pakai |
|---|---|---|
service_started | Container dependency berjalan | Bawaan, sama dengan bentuk pendek |
service_healthy | Healthcheck dependency lolos | Database, cache, message broker |
service_completed_successfully | Container dependency exit 0 | Migrasi, seed data, task sekali jalan |
restart: true memberi efek tambahan: setiap kali dependency di-update, service dependent ikut di-restart. required: false mengubah kegagalan dependency dari error menjadi peringatan.
Compose file yang dipakai untuk menguji ketiga kondisi:
name: kal-compose-demo
services:
db:
image: postgres:17
environment:
POSTGRES_PASSWORD: demo
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 10
start_period: 3s
volumes:
- dbdata:/var/lib/postgresql/data
migrate:
image: alpine:latest
command: sh -c 'echo "migrate berjalan"; sleep 2; echo "migrate selesai"; exit 0'
depends_on:
db:
condition: service_healthy
api:
image: alpine:latest
command: sh -c 'echo "api start"; sleep 300'
depends_on:
migrate:
condition: service_completed_successfully
debug:
image: alpine:latest
command: sh -c 'sleep 300'
profiles:
- debug
depends_on:
db:
condition: service_healthy
volumes:
dbdata:Output docker compose up -d menunjukkan urutan yang benar-benar terjadi:
Container kal-compose-demo-db-1 Started
Container kal-compose-demo-db-1 Waiting
Container kal-compose-demo-db-1 Healthy
Container kal-compose-demo-migrate-1 Starting
Container kal-compose-demo-migrate-1 Started
Container kal-compose-demo-migrate-1 Waiting
Container kal-compose-demo-migrate-1 Exited
Container kal-compose-demo-api-1 Starting
Container kal-compose-demo-api-1 StartedPerhatikan Waiting sebelum Healthy, lalu Waiting sebelum Exited. Baris-baris itulah bukti bahwa Compose benar-benar menunggu, bukan sekadar menjalankan perintah dalam urutan yang benar.
Empat Jebakan yang Terverifikasi
1. service_healthy tanpa healthcheck
Service db tanpa blok healthcheck dipakai sebagai condition: service_healthy:
dependency failed to start: container kal-compose-nohc-db-1 has no healthcheck configuredExit code 1, dan api tidak pernah jalan. Ini galat validasi, bukan menunggu. Perbaiki dengan memakai image yang memang sudah punya HEALTHCHECK, atau tambahkan healthcheck eksplisit di Compose.
2. Migrasi dijalankan ulang di setiap up
Setelah dua kali docker compose up -d, log migrate menunjukkan:
migrate-1 | migrate berjalan
migrate-1 | migrate selesai
migrate-1 | migrate berjalan
migrate-1 | migrate selesaiRestartCount=0, jadi bukan crash. Compose meng-start ulang container yang berhenti. Jadikan command migrasi idempoten, atau tambahkan restart: "no" serta andalkan docker compose run --rm migrate untuk eksekusi manual.
3. Dependency yang gagal tidak otomatis menggagalkan start
migrate keluar dengan kode 3:
service "migrate" didn't complete successfully: exit 3Exit code 1, dan api tetap pada status Created — berhenti ada tapi belum pernah start. Ini perilaku yang benar untuk migrasi: lebih baik gagal cepat daripada menjalankan aplikasi terhadap skema yang belum siap.
4. required: false Bukan Jalan Pintas
Nilai required: false tidak membuat Compose menerima nama service yang tidak ada di file:
service "api" depends on undefined service "db": invalid compose projectrequired: false hanya melonggarkan ketersediaan saat runtime untuk service yang memang dideklarasikan. Yang berubah hanya level responsnya, bukan kelonggaran validasi.
Profiles untuk Layanan Tambahan
profiles memberi nama pada service sehingga hanya start ketika profilnya aktif. Service tanpa profiles selalu ikut start. Field ini tidak menghapus service dari file, hanya mengaktifkan secara selektif, dan cocok untuk tool debugging, admin UI, atau profiler yang tidak perlu ada di setiap environment.
Perintah yang dipakai saat menguji:
| Perintah | Service yang aktif |
|---|---|
docker compose config --services | Semua service tanpa profile |
docker compose --profile debug config --services | Ditambah service ber-profile debug |
docker compose --profile frontend --profile worker config --services | Flag --profile bisa diulang |
COMPOSE_PROFILES="worker,full" docker compose config --services | Env var dengan koma sebagai pemisah |
docker compose config --profiles | Seluruh nama profile yang ada di proyek |
Dua hal yang mudah terlewat. Pertama, docker compose ps tetap menampilkan container ber-profile yang sudah pernah dibuat meski flag profile tidak dipakai, karena ps membaca container yang ada, bukan daftar service aktif. Untuk memeriksa service yang benar-benar aktif, config --services yang tepat.
Kedua, profiled service tidak boleh jadi dependency dari service yang tidak ber-profile. Compose menolak file-nya:
service "core" depends on undefined service "web": invalid compose projectService yang dimatikan profile diperlakukan tidak ada saat validasi. Perbaikannya ada dua: beri profile yang sama ke kedua service, atau set required: false pada depends_on. Cara pertama lebih ketat karena docker compose up tanpa flag akan gagal dengan pesan no service selected, memaksa Anda ingat mengaktifkan profile-nya.
Menunggu Semua Service Healthy: --wait
Untuk CI atau script yang butuh tahu apakah stack siap, iterasi docker compose ps adalah cara yang rapuh. Flag --wait dan --wait-timeout menutup kebutuhan itu. Dokumentasi docker compose up menyebut --wait menunggu service running atau healthy, sekaligus mengaktifkan mode detached.
docker compose up -d --wait --wait-timeout 60Hasil teruji dengan file di atas: exit code 0 dalam 7 detik. Container migrate yang Exited (0) tidak menggagalkan proses. Sebaliknya, healthcheck yang tidak pernah lolos menghasilkan:
application not healthy after 8sdengan exit code 1 tepat setelah 8 detik. Dependency yang unhealthy lebih cepat gagal, sekitar 5 detik pada konfigurasi retries: 2 dan interval: 2s, dengan pesan dependency failed to start: container kal-compose-bad-broken-1 is unhealthy.
Service tanpa
healthcheckdianggap selesai begitu container-nya running. Pada uji di atas,apitidak punya healthcheck dan tetap dilaporkanHealthydi output--wait. Kalau memang butuh--waitberarti sesuatu, definisikanhealthcheckuntuk setiap service yang vital.
Build Multi-stage di Dalam Compose
Setelah urutan start benar, masalah berikutnya adalah ukuran image. Image builder golang:1.26-alpine berukuran 364 MB, dan sebagian besar isinya tidak pernah dibutuhkan saat runtime.
Aplikasi uji memakai HTTP server stdlib Go dengan endpoint /healthz, lalu Dockerfile dua tahap:
FROM golang:1.26-alpine AS build
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY main.go ./
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server .
FROM alpine:latest AS runtime
RUN adduser -D -u 10001 app
COPY --from=build /out/server /usr/local/bin/server
USER app
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/server"]Prinsipnya sederhana: tahap build menyimpan compiler dan seluruh source, tahap runtime hanya menyalin file binary hasil compile. Praktik ini dijelaskan lengkap di dokumentasi multi-stage build Docker. Yang dipindahkan lintas tahap hanyalah /out/server, satu file, bukan direktori kerja yang berisi module cache berukuran ratusan megabyte.
Hasil terukur:
| Item | Nilai |
|---|---|
docker compose build cold | 11 detik |
| Ukuran image akhir | 21,5 MB |
Image builder golang:1.26-alpine | 364 MB |
Image builder dengan docker build --target build | 487 MB |
| Jumlah layer image akhir | 3 |
Layer COPY binary | 5,98 MB |
docker compose exec app id | uid=10001(app) gid=10001(app) groups=10001(app) |
command -v go di container akhir | tidak ada |
docker history mengonfirmasi image akhir hanya punya tiga layer nyata:
5.98MB COPY /out/server /usr/local/bin/server
41kB RUN /bin/sh -c adduser -D -u 10001 app
9.08MB ADD alpine-minirootfs-3.24.2-x86_64.tar.gz /BuildKit menyimpan image builder sebagai cache dan tidak mengirimkannya ke registry, jadi ukuran image yang masuk registry tetap 21,5 MB meski proses build pernah memakai image 364 MB.
Compose mengatur build lewat section build, dan target memilih tahap mana yang jadi hasil akhir:
services:
app:
build:
context: ./app
target: runtime
image: kal-compose-demo-app:local
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/healthz"]
interval: 3s
timeout: 2s
retries: 5
start_period: 2starget berguna saat debugging: docker build --target build -t kal-demo-builder:stage ./app menghasilkan image 487 MB dengan toolchain Go utuh, berguna untuk menjalankan test atau debugger di dalam image yang sama tanpa mengubah Dockerfile.
Urutan COPY dan Efektivitas Cache
Urutannya menentukan berapa banyak yang perlu di-build ulang. Dua Dockerfile diuji dengan perubahan yang sama pada main.go:
| Pola Dockerfile | Waktu build kedua | Langkah CACHED |
|---|---|---|
COPY go.mod lalu go mod download lalu COPY main.go | 12 detik | 4 |
COPY . . lalu go mod download | 17 detik | 1 |
Pada pola kedua, go mod download dieksekusi ulang karena layer sebelumnya ikut berubah. Selisih waktunya kecil di proyek tanpa dependency, tapi jumlah langkah CACHED menunjukkan dependency download tidak lagi dipakai cache. Pada proyek dengan dependency banyak, mount cache module jauh lebih hemat. Pola anti-pattern lain adalah menyalin .git atau node_modules tanpa .dockerignore, yang membuat context build besar dan membatalkan cache setiap kali ada file tak relevan yang berubah.
Rekomendasi Praktis
- Pakai
condition: service_healthyuntuk dependency yang punya endpoint siap, dan definisikanhealthchecksecara eksplisit agar tidak bergantung pada image. - Pakai
condition: service_completed_successfullyuntuk migrasi, dan buat command-nya idempoten. - Pakai
restart: truehanya bila dependent benar-benar perlu start ulang setelah dependency berubah; pada pipeline,docker compose up -d --force-recreatelebih mudah dikontrol. - Gunakan
profilesuntuk layanan tambahan, dan jangan pernah menjadikan profiled service sebagai dependency dari service tanpa profile. - Di CI, pakai
up -d --wait --wait-timeoutdengan batas waktu eksplisit daripada menunggu tanpa timeout. - Susun Dockerfile multi-stage dengan
COPYspesifik lebih dulu agar cache layer tetap berlaku, dan jalankan container sebagai user non-root.
Kesimpulan
Compose sudah menyediakan alat yang tepat untuk masalah urutan start, tapi semua alat hanya bekerja kalau deklarasinya lengkap. depends_on mengatur kapan container dimulai, healthcheck menentukan kapan layanannya bisa dipakai, dan service_completed_successfully menangani container yang sekali jalan. Profiles menjaga environment tetap ramping, --wait menggantikan polling manual, dan build multi-stage memangkas ukuran image serta permukaan serangan tanpa mengorbankan kemampuan build. Mulailah dengan satu Compose file yang punya healthcheck di setiap dependency vital; sebagian besar kegagalan start yang bobok saat debugging berawal dari deklarasi yang tidak lengkap, bukan dari bug Compose.