1. Home
  2. Insights
  3. Docker
  4. Docker Healthcheck สำคัญยังไง และเขียนแบบไหนไม่หลอกตัวเอง
Docker

Docker Healthcheck สำคัญยังไง และเขียนแบบไหนไม่หลอกตัวเอง

อธิบายว่า Docker healthcheck มีไว้ทำอะไร ต่างจาก container running อย่างไร และควรออกแบบ health endpoint กับ healthcheck command แบบไหนให้สะท้อนสภาพพร้อมใช้งานของระบบจริง

Docker Healthcheck สำคัญยังไง และเขียนแบบไหนไม่หลอกตัวเอง

หลายทีมเริ่มใช้ Docker แล้วรู้สึกว่าถ้า container ยังรันอยู่ ก็น่าจะถือว่าแอปยังโอเคอยู่ แต่พอขึ้น production จริง เราจะเริ่มเห็นความต่างระหว่างคำว่า “process ยังไม่ตาย” กับคำว่า “ระบบยังพร้อมรับงาน”

ตัวอย่างที่เจอบ่อยมีแบบนี้

  • Node.js process ยังรันอยู่ แต่ event loop ค้าง
  • server ตอบ port ได้ แต่ route สำคัญใช้ไม่ได้
  • app start ขึ้นแล้ว แต่ยังเชื่อม database ไม่ได้
  • worker process ยังมีชีวิต แต่หยุดหยิบงานจาก queue
  • container ตอบ 200 OK จาก endpoint ที่ไม่สะท้อนอะไรจริง
  • orchestration layer เห็นว่า container healthy ทั้งที่ผู้ใช้ใช้งานไม่ได้แล้ว

ปัญหาไม่ได้อยู่ที่มีหรือไม่มี healthcheck อย่างเดียว แต่อยู่ที่ healthcheck นั้นกำลังวัดอะไร

ถ้าวัดแค่ว่า process ยังอยู่ มันอาจหลอกตัวเอง
ถ้าวัดหนักเกินไป มันอาจสร้าง false alarm
ถ้าวัดผิดชั้น ระบบ orchestration จะตัดสินใจผิด เช่น restart ถี่เกิน หรือปล่อย traffic ไปยัง instance ที่จริง ๆ ใช้งานไม่ได้

บทความนี้อธิบายว่า Docker healthcheck สำคัญยังไง และควรเขียนแบบไหนให้สะท้อน “ความพร้อมใช้งานจริง” มากกว่าการเช็กแบบพอมีไปก่อน

TL;DR

สรุปให้สั้นที่สุด

container ที่ยัง running ไม่ได้แปลว่า service ยังพร้อมใช้งาน
Docker healthcheck จึงมีไว้ช่วยบอกว่า container นั้น “ยังอยู่ในสภาพที่ควรรับงานต่อหรือไม่”

แต่ healthcheck ที่ดีไม่ใช่แค่ ping process หรือเช็กว่า port เปิด
มันต้องสะท้อนระดับความพร้อมที่เหมาะกับบทบาทของ service นั้นจริง ๆ

Docker Healthcheck คืออะไร

Docker healthcheck คือกลไกที่ให้ container รันคำสั่งเป็นระยะ เพื่อประเมินสภาพของตัวเอง แล้ว Docker จะตีความผลลัพธ์ว่า container นั้นอยู่ในสถานะประมาณนี้

  • starting
  • healthy
  • unhealthy

แนวคิดนี้ช่วยแยกระหว่าง

  • container ยังรันอยู่
  • กับ container ยังทำหน้าที่ของมันได้ดีพอหรือไม่

ในแง่ของระบบ production นี่เป็นเรื่องสำคัญมาก เพราะ process จำนวนมากสามารถ “ยังไม่ตาย” แต่ “ใช้งานจริงไม่ได้” ได้เป็นเวลานาน

ทำไมแค่ process ยังรันอยู่ไม่พอ

ลองนึกภาพ Node.js API ที่ bind port สำเร็จแล้ว แต่หลังจากนั้น dependency หลักเริ่มมีปัญหา เช่น

  • DB pool ตายหมด
  • outbound provider timeout จน thread pool หน่วง
  • event loop lag สูงจน request ใหม่แทบไม่ถูกประมวลผล
  • app อยู่ใน state แปลก ๆ หลัง boot
  • critical config โหลดไม่ครบ

จากมุมของ Docker process ยังมีชีวิตอยู่
จากมุมของผู้ใช้ ระบบอาจใช้ไม่ได้แล้ว

นี่คือเหตุผลว่าทำไมคำว่า running กับ healthy ต้องถูกมองเป็นคนละเรื่อง

Healthcheck ช่วยอะไรใน production

ประโยชน์ของ healthcheck ไม่ได้อยู่แค่ “มีสถานะสวยขึ้น” แต่ช่วยหลายเรื่องพร้อมกัน

อย่างแรกคือช่วยให้ระบบมองเห็นว่า instance ไหนพร้อมรับงานจริง
อย่างที่สองคือช่วยให้ orchestration หลีกเลี่ยงการส่ง traffic ไปยัง instance ที่ไม่พร้อม
อย่างที่สามคือช่วยให้ incident ถูกพบเร็วขึ้นในบางกรณี
อย่างที่สี่คือช่วยแยกระหว่างปัญหา boot ช้า กับปัญหาเสียจริง

โดยเฉพาะในระบบที่มีหลาย containers หรือหลาย replicas healthcheck ที่ดีจะช่วยลดการเดาว่าปัญหาอยู่ที่ image, app startup, dependency หรือ runtime degradation

แต่ healthcheck ก็ทำให้หลอกตัวเองได้เหมือนกัน

นี่คือจุดที่สำคัญมาก

หลายระบบมี healthcheck จริง แต่ healthcheck นั้นแทบไม่มีความหมาย เช่น

  • แค่ตอบ 200 OK
  • แค่เช็กว่า Node.js process ยังอยู่
  • แค่เช็กว่า route /health ถูกเรียกได้
  • แค่เช็กว่า HTTP server bind port สำเร็จ

ถ้า endpoint แบบนี้ไม่สะท้อนว่า service ยังทำงานสำคัญของมันได้จริงหรือไม่ ระบบก็จะดู healthy ทั้งที่จริงอาจพังอยู่

ในอีกด้านหนึ่ง ถ้า healthcheck เขียนหนักเกินไป เช่นไป query dependency ภายนอกทุกตัวทุกครั้ง มันก็อาจสร้าง false negative และทำให้ orchestration restart container แบบไม่จำเป็น

ดังนั้น healthcheck ที่ดีต้อง “ไม่เบาเกินจนไม่มีความหมาย” และ “ไม่หนักเกินจนกลายเป็น self-DDoS”

ต้องแยกให้ชัดก่อนว่า healthcheck กำลังใช้เพื่ออะไร

ก่อนเขียน endpoint หรือ command ควรถามให้ชัดว่า healthcheck นี้ใช้ในบริบทไหน

  • ใช้แค่บอกว่า process ยังไม่ค้าง
  • ใช้บอกว่า app boot เสร็จแล้ว
  • ใช้บอกว่า service พร้อมรับ traffic
  • ใช้บอกว่า dependency สำคัญยังพร้อม
  • ใช้บอกว่า worker ยังทำงานปกติ

คำตอบพวกนี้สำคัญ เพราะ service แต่ละแบบควรมี health semantics ต่างกัน

Liveness กับ Readiness คิดแยกไว้จะช่วยมาก

แม้ Docker healthcheck เองไม่ได้แยกศัพท์แบบ Kubernetes ตรง ๆ แต่ในเชิงออกแบบ การแยกความคิดแบบนี้ช่วยมาก

Liveness

ตอบคำถามว่า

process นี้ยังมีชีวิตและยังไม่ค้างจนควรถูก restart หรือไม่

เช่น

  • event loop ยังตอบได้
  • process ยังไม่ deadlock
  • service หลักยังรับ basic probe ได้

Readiness

ตอบคำถามว่า

service นี้พร้อมรับ traffic จริงหรือยัง

เช่น

  • app boot เสร็จแล้ว
  • config สำคัญโหลดครบ
  • dependency ขั้นต่ำพร้อม
  • migration ที่จำเป็นเสร็จแล้ว
  • route สำคัญยังให้บริการได้

ในระบบที่เรียบง่าย คุณอาจรวมสองอย่างไว้ endpoint เดียวก่อนก็ได้ แต่ในเชิง reasoning ควรแยกในหัวให้ชัด

Healthcheck สำหรับ API ควรเช็กอะไรบ้าง

ไม่มีสูตรเดียวที่ใช้ได้กับทุกระบบ แต่แนวทางที่ดีคือเช็กในระดับ “minimum viable readiness”

ตัวอย่างที่พอดีอาจมีพวกนี้

  • app boot เสร็จแล้ว
  • main server loop ยังตอบได้
  • config สำคัญมีครบ
  • dependency ภายในที่จำเป็นจริงยังใช้ได้
  • state ของแอปไม่อยู่ใน degraded mode ที่ไม่ควรรับ traffic

สิ่งที่ต้องระวังคืออย่าทำให้ health endpoint กลายเป็น integration test ขนาดย่อมของทุกอย่างในโลก เพราะนั่นจะทำให้ probe แพงและเปราะเกินไป

เช็ก database ทุกครั้งดีไหม

คำตอบคือ “ขึ้นอยู่กับบทบาทของ service”

ถ้า service นี้ทำอะไรไม่ได้เลยหาก DB ใช้งานไม่ได้ และไม่มี mode ที่ยังรับ request บางส่วนได้ การมี readiness ที่สะท้อน DB อาจสมเหตุผล

แต่ถ้าคุณเอา query หนัก ๆ ไปวิ่งทุก 5 วินาทีจากทุก replica คุณอาจกำลังสร้าง load เพิ่มโดยไม่จำเป็น

แนวทางที่ดีกว่าคือ

  • เช็กแบบเบามาก
  • หรือเช็กสถานะ connection pool/circuit state ภายใน
  • หรือกำหนด threshold ให้ไม่ไวเกินไปต่อ transient blips

จุดสำคัญคือ dependency check ควรสะท้อน “ความพร้อมขั้นต่ำ” ไม่ใช่ “พิสูจน์ว่าทุกอย่างในระบบสมบูรณ์ 100%”

แล้ว service ที่พึ่ง external API ล่ะ

ถ้า service ของคุณเรียก third-party API เป็นครั้งคราว ไม่ได้แปลว่าต้องเอา third-party นั้นมาเป็นตัวตัดสิน health ทุก probe

เพราะไม่อย่างนั้น third-party หน่วงชั่วคราวอาจทำให้ container ของคุณถูกมองว่า unhealthy ทั้งที่จริงยังควรรับงานอื่นต่อได้

คำถามที่ควรถามคือ

  • ถ้า provider นี้ล่ม ระบบเราควรหยุดรับ traffic ทั้งหมดไหม
  • หรือควรตอบ degraded mode บางอย่างได้อยู่
  • การ probe provider ตลอดเวลาคุ้มกับสัญญาณที่ได้ไหม

หลายกรณี external dependency เหมาะจะอยู่ใน observability/alerting มากกว่าอยู่ใน healthcheck หลัก

Healthcheck ของ API กับ worker ไม่เหมือนกัน

นี่เป็นจุดที่มักถูกพลาด

ถ้า container เป็น API สิ่งที่สนใจคือรับ request ได้ไหม
แต่ถ้า container เป็น worker สิ่งที่ควรสนใจอาจเป็น

  • event loop ยังตอบไหม
  • worker loop ยังดึงงานจาก queue ได้ไหม
  • process ค้างหรือไม่
  • internal heartbeat ยังอัปเดตไหม

ถ้า worker ไม่มี HTTP server เลย การใช้ healthcheck แบบ curl route ก็ไม่เกี่ยวอะไรกับหน้าที่จริงของมัน

ดังนั้น healthcheck ต้องผูกกับ “บทบาทของ process” ไม่ใช่ใช้สูตรเดียวทั้งระบบ

ตัวอย่าง healthcheck ที่หลอกตัวเอง

ตัวอย่างคลาสสิกคือแบบนี้

HEALTHCHECK CMD curl -f http://localhost:3000/ || exit 1

ทำไมมันอาจหลอกตัวเอง

  • route / อาจเป็น static response ที่ไม่สะท้อน readiness
  • app อาจตอบ root route ได้ แต่ route สำคัญพัง
  • worker container อาจไม่มีความหมายกับ HTTP เลย
  • endpoint นี้อาจถูก cache หรือ lightweight เกินไปจนไม่ได้วัดอะไรจริง

ปัญหาไม่ได้อยู่ที่ใช้ curl ไม่ได้ แต่อยู่ที่ endpoint ที่ยิงไปนั้นกำลังวัดอะไร

ตัวอย่าง health endpoint ที่มีความหมายมากกว่า

แนวคิดที่ดีกว่าคือมี endpoint เฉพาะ เช่น /health หรือ /ready ที่ตั้งใจออกแบบให้ตอบตามสถานะจริงของแอป

ตัวอย่างเช่น

  • app boot เสร็จหรือยัง
  • config ขั้นต่ำพร้อมไหม
  • internal state ปกติหรือไม่
  • optional dependencies ไม่ต้องตัดสิน health แต่ report ได้

แบบนี้ทำให้ healthcheck มี semantics ที่คุยกันในทีมได้ชัดกว่า “ยิง root route แล้วผ่าน”

ตัวอย่าง Express/Node.js

ตัวอย่างนี้สื่อแนวคิด health endpoint ที่ไม่เบาจนไม่มีความหมาย และไม่หนักจนไปเช็กทุกอย่างแบบเกินจำเป็น

const express = require("express");

const app = express();

let appStarted = false;
let dbReady = false;

async function bootstrap() {
  await fakeInitConfig();
  await fakeConnectDatabase();

  appStarted = true;
  dbReady = true;
}

app.get("/health", (req, res) => {
  if (!appStarted) {
    return res.status(503).json({
      status: "starting"
    });
  }

  if (!dbReady) {
    return res.status(503).json({
      status: "degraded",
      reason: "database_not_ready"
    });
  }

  return res.status(200).json({
    status: "ok"
  });
});

app.get("/", (req, res) => {
  return res.status(200).send("Hello from app");
});

async function fakeInitConfig() {
  return new Promise((resolve) => setTimeout(resolve, 500));
}

async function fakeConnectDatabase() {
  return new Promise((resolve) => setTimeout(resolve, 500));
}

bootstrap().catch((error) => {
  console.error("Bootstrap failed:", error);
});

app.listen(3000, () => {
  console.log("Server listening on port 3000");
});

ตัวอย่างนี้ไม่ได้สมบูรณ์แบบสำหรับทุกระบบ แต่ช่วยให้เห็นว่าการมี /health ที่ตั้งใจตอบ readiness จริง จะดีกว่าการเช็ก route ไหนก็ได้แบบไม่มี semantics

ตัวอย่าง Dockerfile healthcheck

FROM node:20-alpine

WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
  CMD wget -qO- http://127.0.0.1:3000/health || exit 1

CMD ["node", "server.js"]

แต่ค่าพวก interval, timeout, start-period, retries ก็ควรคิด

ค่าพวกนี้ไม่ควรใส่แบบเดาสุ่ม

start-period

สำคัญมากสำหรับแอปที่ boot ช้า เพราะช่วยกันไม่ให้ Docker มองว่าแอป unhealthy ทั้งที่จริงยังอยู่ในช่วงเริ่มต้น

interval

คือความถี่ในการ probe
ถ้าถี่เกินจะสร้าง load โดยไม่จำเป็น
ถ้าห่างเกินจะจับปัญหาช้า

timeout

ควรสั้นพอที่จะบอกว่า probe นี้ fail จริง แต่ไม่สั้นจนโดน transient latency บ่อยเกินไป

retries

ช่วยกัน false alarms ระยะสั้น
แต่ถ้าตั้งสูงเกินไปก็อาจทำให้ระบบมองปัญหาช้า

ค่าพวกนี้ควรสัมพันธ์กับธรรมชาติของ service ไม่ใช่ copy กันทั้งระบบ

อย่าให้ health endpoint แพงเกินไป

มี anti-pattern ที่เจอบ่อยคือ health endpoint ทำงานหนักมาก เช่น

  • query หลายตาราง
  • เรียกหลาย downstream APIs
  • ตรวจ disk/network แบบละเอียด
  • โหลด config จากหลายแหล่งทุกครั้ง
  • คำนวณอะไรซับซ้อน

ผลคือระบบยิ่งโดน probe ยิ่งหนัก และตอนระบบเริ่มอืด healthcheck กลายเป็นตัวเร่งให้แย่ลงอีก

health endpoint ควรเบา เร็ว และ deterministic พอสมควร

แล้วควรคืนรายละเอียดมากแค่ไหน

สำหรับ internal-only health endpoint คุณอาจคืนรายละเอียดพอให้ debug ได้ เช่น

  • status
  • version
  • boot state
  • dependency summary แบบย่อ

แต่ต้องระวังไม่เปิดเผยข้อมูลมากเกินไปถ้า endpoint นี้เข้าถึงได้กว้าง เช่น topology ภายใน, hostnames, secrets, stack traces หรือรายละเอียดที่ไม่ควรออกสู่ public

หลักคิดง่าย ๆ คือ health endpoint ควร “มีประโยชน์ต่อ operations” แต่ไม่กลายเป็น information leak

Healthcheck กับ orchestration ต้องคิดร่วมกัน

healthcheck จะมีประโยชน์จริงก็ต่อเมื่อคุณรู้ว่าระบบข้างบนมันจะเอาสัญญาณนี้ไปทำอะไรต่อ

เช่น

  • แค่แสดงสถานะใน docker ps
  • ใช้ restart container อัตโนมัติ
  • ใช้ตัดสินใจส่ง traffic หรือไม่
  • ใช้ใน deploy rollout
  • ใช้ใน incident triage

ถ้าคุณไม่คิดต่อว่าระบบ orchestration จะ react ยังไง คุณอาจเขียน healthcheck ที่ technically ถูก แต่ operationally สร้างปัญหา

Healthcheck กับ incident response

เวลามี incident จริง healthcheck ที่ดีช่วยตอบได้เร็วขึ้นว่า

  • instance นี้เริ่มมีปัญหาตอนไหน
  • มันตอบ liveness ได้แต่ไม่พร้อมรับ traffic หรือไม่
  • ปัญหาเกิดจาก boot, dependency หรือ degradation ระหว่างรัน
  • deployment ล่าสุดทำให้ readiness fail หรือไม่

นี่คือเหตุผลว่าทำไม health semantics ที่ชัดจะช่วย incident response มากกว่าการมี endpoint /health แบบตอบ 200 ตลอดเวลา

Request ID เกี่ยวอะไรด้วย

healthcheck เองอาจไม่ได้ต้องใช้ request ID แบบ route ธุรกิจทุกครั้ง แต่การ log health state changes, readiness failures หรือ boot transitions ด้วย context ที่พอเหมาะ จะช่วยมากเวลาตามปัญหา

เช่น

  • startup failed because config missing
  • readiness failed because DB pool unavailable
  • container healthy → unhealthy transition occurred after deploy X

สิ่งเหล่านี้อาจไม่ใช่ request ID แบบ user-facing แต่ยังเป็นส่วนหนึ่งของ traceability ที่ช่วย debugging production ได้มาก

ข้อผิดพลาดที่เจอบ่อย

1) เช็กแค่ว่า process ยังอยู่

แบบนี้มักไร้ความหมายต่อ readiness จริง

2) เช็กหนักเกินไป

จน healthcheck เองกลายเป็นภาระของระบบ

3) ใช้ endpoint เดียวกับ user traffic โดยไม่ได้ออกแบบ

เช่นยิง root path หรือ dashboard route แล้วถือว่าเป็น health

4) ไม่ตั้ง start-period

ทำให้แอปที่ boot ช้าถูกมองว่าเสียตั้งแต่ยังไม่ทันพร้อม

5) ใช้ healthcheck เดียวกันกับทุก service

ทั้ง API, worker, scheduler และ one-off jobs ซึ่งความจริงแต่ละชนิดควรมี semantics ต่างกัน

รีวิวเชิง production-minded

Correctness

healthcheck ที่ดีช่วยสะท้อนสภาพพร้อมใช้งานของ service ได้ใกล้เคียงความจริงมากขึ้น และลดกรณีที่ระบบ “ดูรอด” ทั้งที่ใช้ไม่ได้

Security

โดยตรงแล้ว healthcheck ไม่ใช่ security control แต่ health endpoint ที่เปิดเผยข้อมูลมากเกินไปอาจกลายเป็นช่องทางให้ข้อมูลภายในรั่วได้ จึงควรออกแบบระดับรายละเอียดให้เหมาะกับการเข้าถึง

Efficiency

healthcheck ที่เบาและมีความหมายจะช่วยทั้ง orchestration และ operations แต่ healthcheck ที่หนักเกินไปจะเพิ่ม load โดยไม่จำเป็น

Error handling

ถ้า health semantics ชัด ทีมจะตัดสินใจได้ดีขึ้นตอน rollout, rollback และ incident ว่า container ไหนควรถูกแทนที่ และปัญหาอยู่ชั้นไหนของระบบ

Checklist สั้น ๆ ก่อนใช้ Docker healthcheck ใน production

  • ตอบให้ได้ก่อนว่า healthcheck นี้วัดอะไร
  • แยกในความคิดให้ออกระหว่าง liveness กับ readiness
  • endpoint ที่ใช้ probe สะท้อนบทบาทจริงของ service
  • ไม่ใช้ root route แบบลอย ๆ ถ้าไม่มีความหมาย
  • ไม่เช็ก dependency หนักเกินไปโดยไม่จำเป็น
  • ตั้ง start-period, interval, timeout, retries ตามธรรมชาติของแอป
  • worker กับ API ไม่ใช้ health semantics เดียวกันแบบฝืน ๆ
  • health endpoint ไม่เปิดเผยข้อมูลเกินจำเป็น
  • orchestration ด้านบนเข้าใจสัญญาณนี้ถูก
  • มี logging หรือ observability พอให้ตาม health transitions ได้

บทความที่ควรอ่านต่อ

สรุป

Docker healthcheck สำคัญเพราะมันช่วยแยกระหว่าง container ที่ยังมีชีวิต กับ service ที่ยังพร้อมทำงานจริง แต่ประโยชน์นี้จะเกิดขึ้นก็ต่อเมื่อ healthcheck ถูกออกแบบให้วัดสิ่งที่มีความหมาย ไม่ใช่แค่ทำให้สถานะดูเขียว

สรุปสั้นที่สุดคือ

healthcheck ที่ดีต้องไม่เบาจนหลอกตัวเอง และไม่หนักจนกลายเป็นภาระของระบบ

💬 Chat (ตอบเร็ว)