Home / DevOps, Cloud, and Developer Tools / Docker Compose: …

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_on tanpa condition hanya menunggu container dependent benar-benar running.
  • service_healthy membuat Compose menunggu healthcheck dependency benar-benar lolos.
  • Dependency tanpa blok healthcheck membuat service_healthy gagal dengan error, bukan menunggu.
  • service_completed_successfully dipakai untuk container sekali jalan seperti migrasi.
  • restart: true me-restart service dependent setiap kali dependency di-update.
  • profiles menyembunyikan service tambahan sampai profilnya diaktifkan.
  • --wait dan --wait-timeout menunggu 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.

KomponenNilai TerujiKeterangan
Docker Engine29.8.1Klien dan daemon versi sama
Docker Composev5.5.1Plugin docker-compose-plugin
Image ujipostgres:17, alpine:latest 13 MB, golang:1.26-alpine 364 MBPostgres 17, Alpine 3.24.2
Waktu build multi-stage11 detik (cold)Docker BuildKit bawaan

Field healthcheck di Compose mengikuti aturan dan nilai default yang sama dengan instruksi HEALTHCHECK di Dockerfile, dan Compose bisa menimpanya. Lihat referensi HEALTHCHECK untuk detail sisi Dockerfile.

Masalah Nyata: Dependency yang Hanya Diberntai

Dua container dengan depends_on bentuk pendek dianggap aman oleh banyak orang:

yaml
services:
  api:
    image: alpine:latest
    command: sh -c 'sleep 300'
    depends_on:
      - db

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD: demo

Urutan 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.

Healthcheck adalah perintah yang dijalankan berulang di dalam container untuk menentukan apakah layanannya masih bisa dipakai. Bentuk referensinya:

yaml
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
FieldArtiCatatan Teruji
testPerintah cekBentuk list wajib diawali NONE, CMD, atau CMD-SHELL
intervalJarak antar pemeriksaanDuration, bukan detik polos
timeoutBatas waktu satu pemeriksaanKeluarannya bukan 0 dihitung sebagai gagal
retriesJumlah kegagalan berturut-turut sebelum unhealthyDipakai saat condition: service_healthy
start_periodMasa tenggang saat startKegagalan di masa ini tidak dihitung sebagai unhealthy
start_intervalInterval khusus selama start_periodBawaan: 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:

text
$ 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.

conditionCompose menunggu sampaiKasus pakai
service_startedContainer dependency berjalanBawaan, sama dengan bentuk pendek
service_healthyHealthcheck dependency lolosDatabase, cache, message broker
service_completed_successfullyContainer dependency exit 0Migrasi, 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:

yaml
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:

text
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 Started

Perhatikan 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:

text
dependency failed to start: container kal-compose-nohc-db-1 has no healthcheck configured

Exit 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:

text
migrate-1  | migrate berjalan
migrate-1  | migrate selesai
migrate-1  | migrate berjalan
migrate-1  | migrate selesai

RestartCount=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:

text
service "migrate" didn't complete successfully: exit 3

Exit 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:

text
service "api" depends on undefined service "db": invalid compose project

required: 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:

PerintahService yang aktif
docker compose config --servicesSemua service tanpa profile
docker compose --profile debug config --servicesDitambah service ber-profile debug
docker compose --profile frontend --profile worker config --servicesFlag --profile bisa diulang
COMPOSE_PROFILES="worker,full" docker compose config --servicesEnv var dengan koma sebagai pemisah
docker compose config --profilesSeluruh 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:

text
service "core" depends on undefined service "web": invalid compose project

Service 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.

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.

bash
docker compose up -d --wait --wait-timeout 60

Hasil 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:

text
application not healthy after 8s

dengan 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 healthcheck dianggap selesai begitu container-nya running. Pada uji di atas, api tidak punya healthcheck dan tetap dilaporkan Healthy di output --wait. Kalau memang butuh --wait berarti sesuatu, definisikan healthcheck untuk 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:

dockerfile
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:

ItemNilai
docker compose build cold11 detik
Ukuran image akhir21,5 MB
Image builder golang:1.26-alpine364 MB
Image builder dengan docker build --target build487 MB
Jumlah layer image akhir3
Layer COPY binary5,98 MB
docker compose exec app iduid=10001(app) gid=10001(app) groups=10001(app)
command -v go di container akhirtidak ada

docker history mengonfirmasi image akhir hanya punya tiga layer nyata:

text
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:

yaml
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: 2s

target 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 DockerfileWaktu build keduaLangkah CACHED
COPY go.mod lalu go mod download lalu COPY main.go12 detik4
COPY . . lalu go mod download17 detik1

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_healthy untuk dependency yang punya endpoint siap, dan definisikan healthcheck secara eksplisit agar tidak bergantung pada image.
  • Pakai condition: service_completed_successfully untuk migrasi, dan buat command-nya idempoten.
  • Pakai restart: true hanya bila dependent benar-benar perlu start ulang setelah dependency berubah; pada pipeline, docker compose up -d --force-recreate lebih mudah dikontrol.
  • Gunakan profiles untuk layanan tambahan, dan jangan pernah menjadikan profiled service sebagai dependency dari service tanpa profile.
  • Di CI, pakai up -d --wait --wait-timeout dengan batas waktu eksplisit daripada menunggu tanpa timeout.
  • Susun Dockerfile multi-stage dengan COPY spesifik 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.