Healthcheck Docker Compose yang Benar dan Siap Dipakai
Pahami cara Compose mengevaluasi healthcheck, mengapa depends_on tidak menunggu kesiapan layanan, serta contoh pemeriksaan readiness untuk Postgres dan aplikasi.
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 kode keluar, lalu menyimpan satu status pada container: starting, healthy, atau unhealthy. Kode keluar 0 berarti sehat. Kode keluar lainnya berarti tidak sehat, sedangkan kode keluar 2 dicadangkan oleh Docker. Karena itu, jangan pernah mengembalikan kode tersebut dengan sengaja.
Itulah seluruh mekanismenya. Hampir semua masalah healthcheck berasal dari masalah 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, dan melanjutkan dari titik ketika stack dimulai 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. Daftar yang diawali CMD menjalankan perintah secara langsung tanpa shell, sehingga pipe, &&, dan ekspansi variabel tidak berfungsi. Daftar yang diawali CMD-SHELL meneruskan bagian selanjutnya sebagai satu string ke /bin/sh -c di dalam container. Gunakan bentuk ini jika pemeriksaan memerlukan sintaks shell. String biasa diperlakukan sebagai CMD-SHELL. Daftar yang hanya berisi ["NONE"] menghapus healthcheck yang ditanamkan oleh image melalui Dockerfile.
Pemeriksaan berjalan di dalam container, sehingga setiap binary yang disebutkan harus tersedia di dalam image tersebut. Verifikasi hal ini terlebih dahulu. Image minimal yang tidak memiliki curl akan 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 ditemukan menghasilkan OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Image berbasis Alpine biasanya menyediakan wget dari BusyBox, sehingga pemeriksaannya menjadi ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].
Cara interval, retries, dan start_period digabungkan
Lima pengaturan mengontrol waktu. Nilai default-nya berasal dari Docker Engine, bukan dari Compose.
interval: jeda antara dua pemeriksaan setelah container melewati periode mulai. 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 periode mulai. Default 5s, dan memerlukan Docker Engine 25.0 atau yang lebih baru.
Aturan pentingnya adalah: selama periode mulai, pemeriksaan yang gagal tidak dihitung terhadap retries, dan container tetap berada dalam status starting. Saat pemeriksaan berhasil untuk pertama kalinya, container berubah menjadi healthy dan periode mulai langsung berakhir, meskipun sebagian besar waktunya belum digunakan. Jika periode mulai berakhir saat pemeriksaan masih gagal, penghitungan normal dimulai, dan container memerlukan retries kegagalan berturut-turut sebelum ditandai sebagai unhealthy.
Jadi, waktu terlama dari container dimulai hingga unhealthy adalah start_period ditambah retries dikalikan interval, lalu ditambah timeout. Dengan nilai dalam file di atas, hasilnya adalah 30 ditambah 5 kali 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 umum di sini adalah menaikkan retries untuk mengatasi proses mulai yang lambat. Cara ini berhasil sekali, tetapi kemudian selalu menimbulkan dampak buruk: layanan yang memerlukan 8 percobaan ulang untuk mulai kini menoleransi 8 kegagalan berturut-turut di production sebelum masalah terdeteksi. Gunakan start_period sebagai gantinya, karena pengaturan tersebut hanya berlaku sebelum keberhasilan pertama.
Mengapa depends_on sendiri tidak menjamin apa pun
Bentuk singkat depends_on adalah sumber sebagian besar kebingungan.
api:
depends_on:
- dbArtinya hanya satu: mulai container db sebelum container api. Compose menunggu sampai container dibuat dan dimulai. Compose tidak menunggu PostgreSQL menyelesaikan inisialisasi pertama, dan tidak menunggu port 5432 menerima koneksi. Aplikasi Anda mulai sekitar satu detik kemudian, mencoba terhubung ke port yang belum menerima koneksi, lalu berhenti. Dalam log, Anda melihat Connection refused, atau FATAL: the database system is starting up saat server sudah aktif tetapi masih dalam proses pemulihan.
Bentuk panjangnya adalah 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 layanan dependen sampai dependensi melaporkan status sehat. Ini hanya bermakna jika dependensi tersebut mendefinisikan healthcheck, baik di file compose maupun di image-nya. service_completed_successfully menunggu container sekali jalan, seperti migrasi database, selesai dengan status 0.
Dua kolom tambahan berada di samping condition. restart: true memberi tahu Compose untuk memulai ulang layanan ini setelah Compose memperbarui layanan dependensi. required: false mengubah dependensi yang tidak ditemukan dari kesalahan menjadi peringatan.
Sekarang batasan yang sering mengecoh pengguna. Kondisi ini dievaluasi saat stack dijalankan. Kondisi tersebut hanya mengatur urutan mulai, bukan aturan supervisi. Jika database dimulai ulang pada pukul tiga pagi, tidak ada evaluasi ulang terhadap service_healthy dan tidak ada tindakan untuk memulai ulang aplikasi Anda agar kondisi tersebut terpenuhi kembali. Kode aplikasi Anda tetap harus terhubung kembali sendiri. docker compose up --no-deps api sengaja melewati seluruh mekanisme ini. Hal yang sama berlaku saat container dimulai 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 itu tidak membuktikan bahwa layanan dapat menjawab permintaan. Aplikasi web dapat mempertahankan socket listening tetap terbuka lama setelah connection pool database-nya berhenti, dan pemeriksaan proses tetap berstatus berhasil selama seluruh gangguan layanan.
Minta container melakukan tugas yang menjadi fungsinya:
- Untuk layanan HTTP, minta endpoint nyata.
curl -fsSberakhir dengan status non-zero pada status 400 atau lebih tinggi karena-f, sehingga respons 500 dari aplikasi yang rusak dianggap sebagai pemeriksaan yang gagal. - Untuk PostgreSQL, gunakan
pg_isready. Perintah ini berakhir dengan status 0 saat server menerima koneksi, 1 saat server menolaknya, 2 saat server sama sekali tidak merespons, dan 3 saat parameter yang Anda berikan salah. - Untuk Redis, gunakan
redis-cli ping. Perintah ini mencetakPONGdan berakhir dengan status 0. - Untuk MariaDB, image resmi menyediakan skrip
healthcheck.sh, danhealthcheck.sh --connect --innodb_initializedadalah format yang didokumentasikan oleh pengelolanya.
pg_isready memiliki satu hal yang perlu diperhatikan. Pada proses start pertama dengan direktori data kosong, image postgres resmi menjalankan inisialisasinya terhadap server sementara yang hanya listening pada Unix socket. pg_isready tanpa argumen host menggunakan socket tersebut. Akibatnya, perintah itu dapat menjawab "accepting connections" saat port TCP 5432 masih tertutup bagi aplikasi Anda. Arahkan pemeriksaan secara eksplisit ke TCP agar masalah ini teratasi, karena server sementara tidak merespons 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 pengetikan. Compose memperluas $VAR sendiri saat membaca file, sehingga nilai dari environment host Anda akan dimasukkan ke dalam pemeriksaan. $$ meng-escape nilai tersebut menjadi satu $, sehingga shell di dalam container memperluasnya berdasarkan environment milik container itu sendiri.
Stack postgres dan 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 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 ada dalam daftar karena Compose belum membuatnya.
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, masing-masing dengan waktu mulai, waktu selesai, ExitCode, dan Output dari perintah tersebut. Output yang disimpan dipotong. Karena itu, pemeriksaan yang mencetak isi halaman yang besar akan menghasilkan entri log yang tidak berguna. Buat pemeriksaan tetap ringkas.
Tindakan Docker saat container menjadi tidak sehat
Tidak ada. Inilah jawaban yang paling mengejutkan banyak orang.
Docker Engine pada satu host tidak memulai ulang container yang tidak sehat. Kebijakan restart: unless-stopped bereaksi saat proses utama berhenti, sedangkan container yang tidak sehat belum berhenti. Container tersebut dapat tetap berada dalam status unhealthy selama seminggu sementara Compose membiarkannya. Swarm mode mengganti task yang tidak sehat, tetapi stack Compose biasa pada satu server tidak melakukannya.
Ada dua pilihan yang dapat diambil. Buat proses berhenti saat mengetahui bahwa dirinya mengalami kerusakan, sehingga kebijakan restart memiliki kondisi untuk ditindaklanjuti. Atau pantau status dari luar dan buat peringatan untuk kondisi tersebut. Mengarahkan monitor Uptime Kuma ke endpoint yang sama dengan yang dipanggil healthcheck membuat dependensi yang rusak terlihat di kedua tempat. Anda juga akan mengetahuinya dari monitor, bukan dari pengguna. Jika traffic mencapai aplikasi melalui reverse proxy Traefik, ingat bahwa tampilan proxy terhadap backend terpisah dari status kesehatan Docker. Jadi, salah satunya tidak menggantikan yang lain.
Men-debug pemeriksaan yang tidak pernah menjadi sehat
Jalankan perintah yang sama persis secara manual, di container yang sama, lalu periksa kode keluarnya:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"exit=0 saat container masih melaporkan status tidak sehat berarti test compose Anda berbeda dari yang baru saja diketik. Biasanya, CMD digunakan saat sintaks shell diperlukan.
Dua kesalahan berikut mencakup sebagian besar kasus lainnya. Kesalahan pertama adalah port yang salah. Healthcheck berjalan di dalam container, jadi harus menggunakan port container, bukan port host yang dipublikasikan. Dengan ports: - "8080:3000", aplikasi mendengarkan pada port 3000. Pemeriksaan terhadap http://localhost:8080 akan terus gagal, meskipun situs berfungsi dengan baik di browser. Kesalahan kedua adalah host yang salah. Di dalam pemeriksaan, localhost adalah container itu sendiri. Ini benar untuk memeriksa container tersebut, tetapi salah untuk memeriksa container lain. Untuk itu, Anda memerlukan nama service, misalnya db.
Ada satu kasus terakhir yang perlu disebutkan: healthcheck berhasil, tetapi pengguna melihat kesalahan. Hal ini terjadi ketika endpoint mengembalikan status 200 statis tanpa mengakses komponen yang sebenarnya. Endpoint kesiapan yang tidak pernah melakukan kueri ke database tidak dapat menunjukkan bahwa database sudah tidak tersedia. Buat endpoint tersebut menjalankan satu kueri nyata yang ringan.
FAQ
Mengapa aplikasi saya tetap gagal terhubung ketika depends_on menyatakan bahwa database dalam kondisi sehat?
Karena condition: service_healthy dievaluasi satu kali saat stack dimulai. Setelah itu, condition: service_healthy tidak memantau apa pun. Jika container database dimulai ulang setelahnya, Compose tidak memulai ulang aplikasi Anda untuk memenuhi kondisi tersebut kembali. Karena itu, kode aplikasi Anda memerlukan logika koneksi ulang dan percobaan ulang sendiri. Kondisi ini juga tidak berpengaruh saat Anda memulai satu container dengan docker start atau docker compose up --no-deps.
Apakah saya memerlukan healthcheck jika image sudah mendefinisikannya?
Biasanya tidak. Menimpanya sering kali justru merupakan langkah yang keliru karena pengelola image mengetahui arti readiness untuk perangkat lunak tersebut. Tambahkan healthcheck sendiri hanya jika pemeriksaan dari image tidak sesuai dengan pengaturan Anda, misalnya jika pemeriksaan tersebut menguji port yang telah Anda pindahkan. Untuk menonaktifkan healthcheck dari image, tetapkan test: ["NONE"] atau disable: true pada service.
Sebaiknya healthcheck menggunakan curl atau wget?
Gunakan yang sudah tersedia di dalam image, lalu pastikan keberadaannya dengan docker compose exec <service> curl --version sebelum mengandalkannya. Banyak image berbasis Debian tidak memiliki keduanya. Image berbasis Alpine memiliki wget dari BusyBox. Jangan menambahkan paket ke image hanya untuk menjalankan healthcheck jika perangkat lunak tersebut menyediakan client sendiri, seperti pg_isready atau redis-cli.
Apakah container yang tidak sehat 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 tidak sehat tetap berjalan dan tetap bermasalah sampai ada tindakan dari komponen lain. Buat proses berhenti ketika mendeteksi kegagalan, atau jalankan monitor eksternal yang mengirimkan peringatan berdasarkan status tersebut.
Berapa lama nilai start_period seharusnya?
Tetapkan cukup lama untuk mencakup waktu mulai pertama yang sah dan paling lambat yang telah Anda ukur, ditambah margin. Ukur waktunya dengan docker compose up menggunakan volume kosong karena proses mulai pertama database jauh lebih lambat daripada setiap proses mulai berikutnya. start_period yang terlalu lama hanya menunda hasil unhealthy pertama. Jumlah percobaan ulang yang terlalu tinggi melemahkan pemeriksaan selama seluruh masa hidup container, dan ini merupakan kegagalan yang lebih buruk.