Multi-stage build
เรียนรู้การแยกขั้นตอน build ออกจาก runtime ใน Dockerfile เพื่อแก้ปัญหา image ใหญ่เกินจำเป็น ลดไฟล์รก เพิ่มความปลอดภัย และทำให้ส่ง image ได้เร็วขึ้น
ภาพจำสำคัญบทนี้
Multi-stage build = FROM หลายครั้ง + COPY --from เฉพาะ artifact | เป้าหมายคือ runtime image ต้องเล็ก สะอาด และปลอดภัย
ส่วนที่ 1
1. Multi-stage build คืออะไร
Multi-stage build คือเทคนิคเขียน Dockerfile ให้มีหลาย stage (หลายช่วง) ในไฟล์เดียว โดยแต่ละ stage รับผิดชอบงานคนละหน้าที่ เช่น stage แรกเอาไว้ build โค้ด และ stage สุดท้ายเอาไว้รันแอปจริง หลักคิดสำคัญคือ: stage สุดท้ายควรมีเฉพาะ "สิ่งที่จำเป็นต่อการรัน" เท่านั้น ไม่ต้องแบกไฟล์ build tool, dependency สำหรับคอมไพล์, test files, หรือ source code ที่ไม่จำเป็นทั้งหมด
- มองเป็นสายพานการผลิต: ช่วงแรกผลิตชิ้นงาน ช่วงสุดท้ายแพ็กเฉพาะของที่จำเป็นส่งถึงผู้ใช้
- ลดขนาด image ได้มาก เพราะไม่คัดลอกของที่ใช้แค่ตอน build ไป runtime
- ทำให้ image สะอาด ดูแลง่าย และ audit ความปลอดภัยได้ง่ายขึ้น
ส่วนที่ 2
2. ปัญหาของ Dockerfile แบบ stage เดียว
Dockerfile แบบ stage เดียวมักมีปัญหาในงานจริง โดยเฉพาะโปรเจกต์ Node.js/Frontend ที่ต้อง build ก่อนรัน ปัญหาคลาสสิกคือ image ใหญ่เกินไป เพราะทุกอย่างอยู่รวมกัน: source code, dev dependencies, build dependencies, เครื่องมือ compile, cache, และไฟล์ที่ไม่จำเป็นสำหรับ production
| ปัญหา | เกิดจากอะไร | ผลกระทบ |
|---|---|---|
| Image ใหญ่เกินไป | stage เดียวเก็บทั้ง build tools + source + output | push/pull ช้า, เปลือง storage, deploy ช้าลง |
| พื้นผิวโจมตีมากขึ้น | มีเครื่องมือ build และแพ็กเกจส่วนเกินอยู่ใน runtime | เพิ่มโอกาสช่องโหว่และการโจมตี |
| โครงสร้างไม่สะอาด | ไฟล์ที่ไม่จำเป็นถูกแบกไป production | debug และ maintain ยากขึ้น |
ภาพที่ 1 — เปรียบเทียบ single-stage กับ multi-stage
ภาพนี้แสดงความต่างชัดเจน: single-stage พกทุกอย่างไปรันจริง แต่ multi-stage เก็บเฉพาะสิ่งที่ต้องใช้ตอน runtime
ส่วนที่ 3
3. แนวคิดของหลาย stage
แนวคิดของหลาย stage คือ "แยกความรับผิดชอบ" ให้ชัดเจนใน Dockerfile: - Build stage: ติดตั้งเครื่องมือและ dependency ที่จำเป็นต่อการ build - Runtime stage: รับเฉพาะ artifact ที่ build เสร็จแล้วเพื่อรันแอป Artifact คือผลลัพธ์ที่พร้อมใช้ เช่นไฟล์ dist/, .next/standalone, static assets หรือไฟล์ binary ที่คอมไพล์แล้ว
ประเด็นที่ 1
Build stage อาจหนักได้ เพราะไม่ถูกส่งไปใช้งานจริง
ประเด็นที่ 2
Runtime stage ควรเบาที่สุดและมีเฉพาะของจำเป็น
ประเด็นที่ 3
Copy เฉพาะ artifact ช่วยลดความซ้ำซ้อนและลดขนาด image
ส่วนที่ 4
4. การใช้ FROM หลายครั้งใน Dockerfile
ใน Dockerfile เราสามารถใช้ FROM ได้หลายครั้ง โดยทุกครั้งจะเริ่ม stage ใหม่ทันที FROM แรกมักเป็น builder (เช่น node:20-alpine) เพื่อ build งาน FROM ถัดไปมักเป็น runtime ที่เบากว่า (เช่น nginx:alpine หรือ node:20-alpine ที่มีเฉพาะ production dependency) ยิ่ง stage สุดท้ายสะอาดเท่าไร image สุดท้ายยิ่งเล็กและปลอดภัยขึ้น
แต่ละ FROM = stage ใหม่
ภาพที่ 2 — Flow: stage แรก build และ stage สุดท้ายใช้เฉพาะผลลัพธ์จำเป็น
Build stage ทำงานหนักได้ ส่วน runtime stage ควรเบาและเหลือเฉพาะสิ่งที่ต้องใช้จริง
ส่วนที่ 5
5. การตั้งชื่อ stage ด้วย AS
แม้จะอ้างอิง stage ด้วยเลขลำดับได้ (เช่น --from=0) แต่แนวทางที่แนะนำคือ "ตั้งชื่อ stage" ด้วย AS เพราะอ่านง่ายกว่าและลดความผิดพลาด เมื่อ Dockerfile โตขึ้น การใช้ชื่อเช่น builder, deps, test, runner จะทำให้ทีมเข้าใจทันทีว่าแต่ละช่วงทำอะไร
- อ่านง่าย: รู้ทันทีว่า stage นี้มีหน้าที่อะไร
- แก้ไขลำดับ stage ได้ง่ายโดยไม่ทำให้ --from พัง
- ลดข้อผิดพลาดจากการอ้างอิงเลข stage ผิด
ส่วนที่ 6
6. การใช้ COPY --from=...
หัวใจของ Multi-stage build คือ COPY --from=... เพื่อคัดลอก artifact จาก stage ก่อนหน้าไป stage สุดท้าย เราไม่ต้องคัดลอก source ทั้งโปรเจกต์ไป runtime แต่เลือกเฉพาะโฟลเดอร์ที่จำเป็นจริง ๆ เช่น dist/ หรือ build/
ภาพที่ 3 — การ copy artifact จาก builder stage ไป runtime stage
COPY --from=... คือกลไกสำคัญที่ทำให้ runtime image มีเฉพาะ artifact ที่จำเป็น
ส่วนที่ 7
7. ตัวอย่าง Dockerfile แบบ stage เดียว
ตัวอย่างนี้เป็น Frontend (เช่น React/Vite) แบบ stage เดียว ซึ่งใช้งานได้ แต่ image มักใหญ่เกินจำเป็น เพราะ runtime ยังมี source และ dependency ที่ไม่ต้องใช้
ส่วนที่ 8
8. ตัวอย่าง Dockerfile แบบ multi-stage
ตัวอย่างเดียวกันแต่ใช้ multi-stage: stage แรก build ด้วย Node.js และ stage สุดท้ายใช้ Nginx เสิร์ฟไฟล์ที่ build เสร็จแล้ว ผลคือ runtime image มีแค่ static files + nginx ไม่ต้องแบก source code และ npm ecosystem ทั้งหมด
ส่วนที่ 9
9. เปรียบเทียบผลลัพธ์ของทั้งสองแบบ
ตัวเลขจริงจะต่างกันตามโปรเจกต์ แต่แนวโน้มมักเป็นแบบนี้: multi-stage เล็กลงชัดเจน และ deploy เร็วกว่า
ใช้คำสั่งนี้เพื่อเทียบขนาด image สองแบบ
| ตัวชี้วัด | Single-stage | Multi-stage |
|---|---|---|
| ขนาด image | ~320MB | ~45MB |
| มี source code ใน runtime | มี | ไม่มี (คงเหลือเฉพาะ artifact) |
| มี build dependencies ใน runtime | มี | ไม่มี |
| เวลา push/pull | ช้ากว่า | เร็วกว่า |
| ความสะอาดของ image | ปานกลาง | สูง |
ส่วนที่ 10
10. อธิบายทีละบรรทัดของตัวอย่าง multi-stage
อ่าน Dockerfile แบบมี mental model จะช่วย debug ได้เร็วขึ้น ลองไล่ทีละบรรทัดจากตัวอย่างด้านบน
| บรรทัด | ทำอะไร | เหตุผล |
|---|---|---|
| FROM node:20-alpine AS builder | เริ่ม stage build | ใช้ image Node สำหรับงาน build |
| WORKDIR /app | กำหนดโฟลเดอร์ทำงาน | คำสั่งต่อไปจะทำงานใน /app |
| COPY package*.json ./ | คัดลอกไฟล์ dependency manifest | แยก layer เพื่อใช้ cache ได้ดีขึ้น |
| RUN npm ci | ติดตั้ง dependencies แบบ reproducible | เหมาะกับ CI และ build ที่เสถียร |
| COPY . . | คัดลอก source code | นำโค้ดทั้งหมดเข้ามาเพื่อ build |
| RUN npm run build | สร้าง artifact | ได้ dist/ สำหรับ production |
| FROM nginx:alpine AS runner | เริ่ม runtime stage | ใช้ image เบาเพื่อ serve static |
| COPY --from=builder /app/dist ... | คัดลอกเฉพาะ artifact | ไม่คัดลอก source/deps ที่ไม่จำเป็น |
| EXPOSE 80 | ระบุพอร์ตที่บริการใช้งาน | เป็น metadata เพื่อสื่อสารให้ผู้ใช้ image |
| CMD ["nginx", "-g", "daemon off;"] | สั่งเริ่ม Nginx | รันเซิร์ฟเวอร์ foreground ตามหลัก container |
ส่วนที่ 11
11. ข้อดี: image เล็กลง ปลอดภัยขึ้น สะอาดขึ้น
Multi-stage build ช่วยได้ 3 มุมหลักที่เห็นผลจริงในทีมพัฒนา
ประเด็นที่ 1
image เล็กลง: ตัด build tools และไฟล์ไม่จำเป็นออกจาก runtime ทำให้ส่งขึ้น registry และดึงลง server ได้เร็วขึ้น
ประเด็นที่ 2
ปลอดภัยขึ้น: ลด package ส่วนเกินใน production ลด attack surface และลดโอกาสมี secret ติดไปโดยไม่ตั้งใจ
ประเด็นที่ 3
สะอาดขึ้น: runtime image มีเฉพาะสิ่งที่ใช้รันจริง ทำให้ทีมใหม่เข้ามา debug ได้ง่าย
ส่วนที่ 12
12. ข้อควรระวัง
ถึงแม้ Multi-stage build จะดีมาก แต่ต้องระวังเรื่อง path, environment และ native dependency ให้ถูกต้อง
- อย่าลืม .dockerignore เพราะ build context ใหญ่ยังคงทำให้ build ช้าได้ แม้ใช้ multi-stage
- ตรวจ path ของ artifact ให้ตรงจริง เช่น /app/dist หรือ /app/.next/standalone
- ถ้ามี native module ต้องเช็กความเข้ากันได้ของ base image (alpine vs debian)
- ระวัง env vars: ควร inject ตอน runtime ไม่ฝัง secret ลง image
- ควร pin version ของ base image เพื่อ build ที่คาดเดาได้
ส่วนที่ 13
13. ข้อผิดพลาดที่พบบ่อย เช่น copy path ผิด หรือ stage name ผิด
จุดที่พลาดบ่อยส่วนใหญ่เกิดจากการอ้างอิง stage หรือ path ไม่ตรงกับผลลัพธ์จริง
| ข้อผิดพลาด | อาการ | วิธีแก้ |
|---|---|---|
| stage name ผิด | COPY --from=build แต่ตั้งชื่อ stage เป็น builder | ใช้ชื่อ stage ให้ตรงกัน และตรวจสะกดทุกจุด |
| copy path ผิด | COPY --from=builder /app/build แต่จริง ๆ ออกเป็น /app/dist | เช็ก output ของคำสั่ง build และปรับ path ให้ตรง |
| ลืม copy runtime config | แอปรันได้แต่ไฟล์ config/nginx.conf ไม่ถูกโหลด | คัดลอกไฟล์ config ที่จำเป็นเข้า runtime stage |
| ใช้ image runtime หนักเกิน | ผลลัพธ์ยังใหญ่แม้ใช้ multi-stage | เลือก base image runtime ที่เล็กและตรงงาน เช่น nginx:alpine |
| ติดตั้ง dev dependency ใน runtime | ขนาด image โตและมี package เกินจำเป็น | ติดตั้งเฉพาะ production dependency ใน stage สุดท้าย |
ส่วนที่ 14
14. สถานการณ์จริงที่เหมาะกับ multi-stage build
งานที่มีขั้นตอน compile/transpile/build ก่อนรัน มักเหมาะกับ multi-stage มากเป็นพิเศษ
ส่วนที่ 15
15. สรุปท้ายบทแบบจำง่าย
จำสูตรสั้น ๆ: "Build หนักได้ แต่ Run ต้องเบา" หรือจำอีกแบบ: "ใช้ FROM หลายครั้ง + COPY --from เฉพาะ artifact" เมื่อคิดแบบนี้ทุกครั้ง คุณจะได้ production image ที่เล็กกว่า สะอาดกว่า และพร้อมใช้งานจริงมากกว่า Dockerfile แบบ stage เดียว
- Multi-stage build = Dockerfile เดียว หลาย stage
- FROM หลายครั้งเพื่อแยกงาน build กับ runtime
- AS ใช้ตั้งชื่อ stage ให้อ่านง่าย
- COPY --from=... ใช้คัดลอกเฉพาะ artifact
- ผลลัพธ์: image เล็กลง ปลอดภัยขึ้น สะอาดขึ้น
ส่วนที่ 16
16. แบบฝึกหัด 4 ข้อ พร้อมแนวเฉลย (กดแล้วค่อยแสดง)
ลองตอบด้วยตัวเองก่อน แล้วค่อยกดเปิดแนวเฉลยเพื่อเช็กความเข้าใจ