Healthcheck Docker Compose yang Benar dan Andal
Pahami evaluasi healthcheck Docker Compose, alasan depends_on tidak menunggu layanan siap, serta contoh readiness check untuk Postgres dan aplikasi Anda.
Apa yang sebenarnya dilakukan healthcheck Docker Compose
Healthcheck Docker Compose adalah satu perintah yang dijalankan Docker di dalam container secara berkala. Docker tidak membaca log, memantau port, atau memeriksa daftar proses Anda. Docker menjalankan perintah tersebut, membaca exit code, lalu menyimpan satu status pada container: starting, healthy, atau unhealthy. Exit code 0 berarti sehat. Exit code lainnya berarti tidak sehat, sedangkan exit code 2 dicadangkan oleh Docker. Karena itu, jangan pernah mengembalikan kode tersebut secara sengaja.
Itulah seluruh mekanismenya. Hampir semua masalah healthcheck memiliki penyebab yang sama: perintah yang Anda tulis menjawab pertanyaan yang berbeda dari pertanyaan yang sebenarnya ingin Anda ajukan. Panduan ini mengasumsikan bahwa Anda sudah mengetahui cara menulis file compose pada VPS, lalu melanjutkan dari tahap ketika stack berjalan dalam urutan yang salah.
services:
api:
image: ghcr.io/example/api:1.4.0
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30sNilai test memiliki dua bentuk yang berguna. List yang diawali CMD menjalankan perintah secara langsung tanpa shell, sehingga pipe, &&, dan ekspansi variabel tidak berfungsi. List yang diawali CMD-SHELL meneruskan bagian setelahnya sebagai satu string ke /bin/sh -c di dalam container. Gunakan bentuk ini jika pemeriksaan memerlukan sintaks shell. String biasa diperlakukan sebagai CMD-SHELL. List yang hanya berisi ["NONE"] menghapus healthcheck yang sudah disertakan oleh image melalui Dockerfile.
Pemeriksaan berjalan di dalam container, sehingga setiap binary yang disebut harus tersedia di dalam image tersebut. Verifikasi hal itu terlebih dahulu. Image yang ramping tanpa curl dapat menghasilkan container yang terus-menerus berstatus tidak sehat, sementara penyebabnya tidak pernah muncul dalam log aplikasi. Uji secara manual:
docker compose exec api curl --versionBinary yang tidak tersedia akan menghasilkan OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Image berbasis Alpine biasanya menyediakan BusyBox wget sebagai gantinya, sehingga pemeriksaannya menjadi ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].
Cara interval, retries, dan start_period saling berinteraksi
Lima pengaturan menentukan waktu pemeriksaan. Nilai default-nya berasal dari Docker Engine, bukan dari Compose.
interval: jeda antara dua pemeriksaan setelah container melewati start period. Default 30s.timeout: durasi maksimum satu pemeriksaan sebelum Docker menghentikannya dan menghitung pemeriksaan tersebut sebagai kegagalan. Default 30s.retries: jumlah kegagalan berturut-turut yang diperlukan sebelum status berubah menjadiunhealthy. Default 3.start_period: masa tenggang setelah container dimulai. Default 0s.start_interval: frekuensi pemeriksaan selama start period. Default 5s, dan memerlukan Docker Engine 25.0 atau yang lebih baru.
Aturan yang penting: selama start period, pemeriksaan yang gagal tidak dihitung terhadap retries, dan container tetap berada dalam status starting. Saat pemeriksaan berhasil untuk pertama kalinya, container menjadi healthy dan start period langsung berakhir, meskipun sebagian besar waktunya belum digunakan. Jika start period berakhir saat pemeriksaan masih gagal, hitung mundur normal dimulai, dan container memerlukan retries kegagalan berturut-turut sebelum ditandai sebagai unhealthy.
Jadi, waktu terlama sejak container dimulai hingga mencapai unhealthy adalah start_period ditambah retries yang dikalikan dengan interval, lalu ditambah timeout. Dengan nilai dalam file di atas, perhitungannya adalah 30 ditambah 5 dikalikan 13, yaitu 95 detik. Catat angka tersebut sebelum menetapkan batas waktu deployment, karena rollout yang berhenti setelah 60 detik tidak akan pernah melihat container ini mencapai status akhir.
Kesalahan yang umum terjadi adalah menaikkan retries untuk mengakomodasi proses start yang lambat. Cara ini berhasil sekali, tetapi kemudian terus menimbulkan dampak: service yang memerlukan 8 percobaan ulang untuk melakukan boot kini menoleransi 8 kegagalan berturut-turut di production sebelum terdeteksi. Gunakan start_period, karena pengaturan ini hanya berlaku sebelum pemeriksaan pertama berhasil.
Mengapa depends_on sendiri tidak menjamin apa pun
Bentuk singkat depends_on merupakan sumber sebagian besar kebingungan.
api:
depends_on:
- dbArtinya hanya satu: jalankan container db sebelum container api. Compose menunggu container dibuat dan dijalankan. Compose tidak menunggu PostgreSQL menyelesaikan inisialisasi pertama, dan tidak menunggu port 5432 menerima koneksi. Aplikasi Anda berjalan sekitar satu detik kemudian, mencoba terhubung ke port yang belum menerima koneksi, lalu berhenti. Dalam log, Anda akan melihat Connection refused, atau FATAL: the database system is starting up jika server sudah aktif tetapi masih dalam proses pemulihan.
Bentuk panjang adalah konfigurasi yang sebenarnya dibutuhkan:
api:
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullycondition memiliki tiga nilai. service_started sama dengan bentuk singkat. service_healthy menahan service dependen sampai dependensi melaporkan status sehat. Fitur ini hanya bermakna jika dependensi tersebut mendefinisikan healthcheck, baik dalam file compose maupun dalam image-nya. service_completed_successfully menunggu container sekali jalan, seperti migrasi database, hingga berhenti dengan status 0.
Dua field tambahan berada di sebelah condition. restart: true memberi tahu Compose untuk me-restart service ini setelah memperbarui service dependensi. required: false mengubah dependensi yang hilang dari error menjadi peringatan.
Sekarang, perhatikan batasan yang sering menimbulkan masalah. Kondisi ini dievaluasi saat stack dijalankan. Kondisi tersebut mengatur urutan start, bukan aturan supervisi. Jika database restart pada pukul tiga pagi, tidak ada evaluasi ulang terhadap service_healthy, dan aplikasi Anda tidak akan di-restart untuk memenuhinya kembali. Kode aplikasi tetap harus melakukan koneksi ulang secara mandiri. docker compose up --no-deps api memang melewati seluruh mekanisme ini, demikian juga menjalankan container secara langsung dengan docker start.
Tulis pemeriksaan yang menguji kesiapan, bukan sekadar keberadaan proses
Pemeriksaan seperti pgrep nginx hanya membuktikan bahwa entri proses ada di tabel proses. Pemeriksaan tersebut tidak membuktikan bahwa service dapat menjawab permintaan. Aplikasi web dapat mempertahankan socket listening tetap terbuka lama setelah connection pool databasenya berhenti, sedangkan pemeriksaan proses tetap berstatus berhasil selama seluruh gangguan berlangsung.
Minta container menjalankan pemeriksaan sesuai fungsi yang seharusnya dilakukannya:
- Untuk service HTTP, minta endpoint yang benar-benar tersedia. Karena
-f,curl -fsSkeluar dengan kode 22 ketika server menjawab dengan status 400 atau lebih tinggi. Jadi, respons 500 dari aplikasi yang rusak dianggap sebagai pemeriksaan yang gagal. Dokumentasi curl sendiri menyebut-ftidak fail-safe, terutama untuk respons autentikasi 401 dan 407. Karena itu, arahkan pemeriksaan ke endpoint yang tidak memerlukan login. - Untuk PostgreSQL, gunakan
pg_isready. Perintah ini keluar dengan kode 0 ketika server menerima koneksi, 1 ketika server menolak koneksi, 2 ketika server sama sekali tidak merespons, dan 3 ketika parameter yang diberikan salah. - Untuk Redis, gunakan
redis-cli ping. Perintah ini mencetakPONGdan keluar dengan kode 0. - Untuk MariaDB, image resmi menyediakan skrip
healthcheck.sh, danhealthcheck.sh --connect --innodb_initializedadalah bentuk yang didokumentasikan oleh pengelolanya.
pg_isready memiliki satu hal yang perlu diperhatikan. Pada start pertama dengan direktori data yang kosong, image resmi postgres menjalankan inisialisasi pada server sementara yang hanya listening pada Unix socket. pg_isready tanpa argumen host menggunakan socket tersebut. Akibatnya, perintah itu dapat menjawab "accepting connections" meskipun port TCP 5432 masih tertutup bagi aplikasi Anda. Arahkan pemeriksaan secara eksplisit ke TCP untuk mengatasi masalah ini, karena server sementara tersebut tidak menjawab melalui TCP.
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30sTanda dolar ganda bukan kesalahan ketik. Compose memperluas $VAR sendiri saat membaca file, sehingga nilai dari environment host Anda akan tertanam ke dalam pemeriksaan. $$ melakukan escape sehingga menjadi satu $, lalu shell di dalam container memperluasnya berdasarkan environment milik container.
Postgres dan stack aplikasi yang dimulai dalam urutan yang benar
services:
db:
image: postgres:17.5
environment:
POSTGRES_USER: appuser
POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
POSTGRES_DB: appdb
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 30s
restart: unless-stopped
api:
image: ghcr.io/example/api:1.4.0
environment:
DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
ports:
- "127.0.0.1:8080:8080"
restart: unless-stopped
volumes:
pgdata:Jalankan stack tersebut dan pantau perubahan statusnya:
docker compose up -d
docker compose psKolom STATUS memuat status kesehatan dalam tanda kurung siku. Pasangan yang sehat menampilkan Up 41 seconds (healthy) pada kedua baris. Saat database masih melakukan inisialisasi, db menampilkan Up 4 seconds (health: starting) dan api belum muncul dalam daftar. Compose sudah membuat container api dan hanya menunda start-nya sampai db melaporkan status sehat. Container tersebut tidak terlihat karena docker compose ps secara default hanya menampilkan container yang sedang berjalan. Jalankan docker compose ps -a pada saat itu, lalu api akan muncul dalam status created.
Untuk mengetahui alasan pemeriksaan berhasil atau gagal, baca log kesehatan:
docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"Docker menyimpan beberapa hasil terakhir. Setiap hasil berisi waktu mulai, waktu selesai, ExitCode, dan Output dari perintah tersebut. Output yang disimpan dipotong. Karena itu, pemeriksaan yang mencetak isi halaman dalam jumlah besar akan menghasilkan entri log yang tidak berguna. Buat pemeriksaan tetap ringkas.
Apa yang dilakukan Docker ketika container berstatus unhealthy
Tidak ada. Inilah jawaban yang paling mengejutkan banyak orang.
Docker Engine pada satu host tidak me-restart container yang berstatus unhealthy. Kebijakan restart: unless-stopped bereaksi ketika proses utama berhenti, sedangkan container yang unhealthy belum berhenti. Container tersebut dapat tetap berada pada unhealthy selama seminggu, sementara Compose membiarkannya. Swarm mode mengganti task yang unhealthy, tetapi stack Compose biasa pada satu server tidak melakukannya.
Dengan demikian, ada dua opsi yang tepat. Hentikan proses ketika proses tersebut mengetahui bahwa dirinya bermasalah, sehingga kebijakan restart memiliki kondisi untuk ditangani. Atau pantau status dari luar dan buat alert untuk kondisi tersebut. Mengarahkan monitor Uptime Kuma ke endpoint yang sama dengan yang dipanggil healthcheck berarti dependensi yang bermasalah akan terlihat di kedua tempat, dan Anda akan mengetahuinya dari monitor, bukan dari pengguna. Jika trafik menuju aplikasi melalui reverse proxy Traefik, ingat bahwa tampilan proxy terhadap backend terpisah dari status health Docker. Jadi, salah satunya tidak menggantikan yang lain.
Men-debug pemeriksaan yang tidak pernah berstatus sehat
Jalankan perintah yang sama persis secara manual, di container yang sama, lalu periksa exit code:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"exit=0 di sini saat container masih melaporkan status tidak sehat berarti test compose Anda berbeda dari yang baru saja Anda ketik, biasanya karena CMD digunakan saat sintaks shell diperlukan.
Dua kesalahan berikutnya mencakup sebagian besar kasus yang tersisa. Kesalahan pertama adalah port yang salah. Healthcheck berjalan di dalam container, sehingga harus menggunakan port container, bukan port host yang dipublikasikan. Dengan ports: - "8080:3000" aplikasi mendengarkan pada port 3000, dan pemeriksaan terhadap http://localhost:8080 akan selalu gagal meskipun situs berfungsi normal di browser. Kesalahan kedua adalah host yang salah. Di dalam pemeriksaan, localhost adalah container yang sama. Ini benar untuk memeriksa container tersebut, tetapi salah untuk memeriksa container lain. Untuk itu, Anda memerlukan nama service, misalnya db.
Satu kasus terakhir perlu diberi perhatian khusus: healthcheck berhasil, tetapi pengguna tetap melihat error. Ini terjadi ketika endpoint mengembalikan status 200 statis tanpa benar-benar memeriksa komponen apa pun. Endpoint readiness yang tidak pernah melakukan query ke database tidak dapat memberi tahu Anda bahwa database sudah tidak tersedia. Buat endpoint tersebut menjalankan satu query nyata yang ringan.
FAQ
Mengapa aplikasi saya tetap gagal terhubung ketika depends_on menyatakan bahwa database sehat?
Karena condition: service_healthy dievaluasi satu kali saat stack dimulai. Setelah itu, condition: service_healthy tidak melakukan pemantauan. Jika container database dimulai ulang setelahnya, Compose tidak memulai ulang aplikasi untuk memenuhi kondisi tersebut kembali. Karena itu, kode aplikasi Anda memerlukan logika reconnect dan retry sendiri. Kondisi ini juga tidak berpengaruh ketika Anda memulai satu container dengan docker start atau docker compose up --no-deps.
Apakah saya memerlukan healthcheck jika image sudah mendefinisikannya?
Biasanya tidak. Menggantinya sering kali justru memperburuk konfigurasi, karena pengelola image memahami arti readiness untuk software tersebut. Tambahkan healthcheck sendiri hanya jika pemeriksaan dari image tidak sesuai dengan setup Anda, misalnya karena pemeriksaan tersebut menggunakan port yang telah Anda pindahkan. Untuk menonaktifkan healthcheck dari image, tetapkan test: ["NONE"] atau disable: true pada service.
Apakah healthcheck harus menggunakan curl atau wget?
Gunakan yang sudah tersedia di dalam image, lalu konfirmasikan keberadaannya dengan docker compose exec <service> curl --version sebelum mengandalkannya. Banyak image berbasis Debian tidak memiliki keduanya. Image berbasis Alpine memiliki BusyBox wget. Jangan menambahkan package ke image hanya untuk menjalankan healthcheck jika software tersebut menyediakan client bawaannya sendiri, seperti pg_isready atau redis-cli.
Apakah container yang unhealthy akan dimulai ulang secara otomatis?
Tidak oleh Docker Engine pada satu host. Kebijakan restart merespons proses yang berhenti, bukan status kesehatan. Karena itu, container yang unhealthy tetap berjalan dan tetap bermasalah sampai ada komponen lain yang mengambil tindakan. Buat proses berhenti ketika mendeteksi kegagalan, atau jalankan monitor eksternal yang memberikan alert berdasarkan status tersebut.
Berapa lama seharusnya start_period?
Tetapkan durasi yang cukup untuk first start sah paling lambat yang telah Anda ukur, lalu tambahkan margin. Ukur durasinya dengan docker compose up pada volume kosong, karena first start database jauh lebih lambat daripada setiap start setelahnya. start_period yang terlalu panjang hanya menunda verdict unhealthy pertama. Jumlah retry yang terlalu tinggi melemahkan pemeriksaan selama seluruh masa hidup container. Ini merupakan kegagalan yang lebih buruk.