เกิดอะไรขึ้น
ในโปรเจกต์หนึ่งของเรา production image โตไปถึง 1.2GB ทั้งที่เป็น Node service ธรรมดา ๆ ตอน deploy เลยช้า เพราะทุกเครื่องต้อง pull image ก้อนใหญ่ และผลสแกนความปลอดภัยก็ขึ้นแพ็กเกจที่แอปตอนรันไม่เคยแตะเลย
พอเปิดดูข้างใน image ยังมี compiler, build tools และ dev dependencies ที่ใช้แค่ตอน build อยู่ครบ เพราะ image สุดท้ายสร้างต่อจาก environment ตัวเดียวกับที่ใช้ compile แอป
ทำไมถึงเป็นแบบนั้น
ทุกคำสั่งใน Dockerfile จะเพิ่มของเข้าไปใน image และ image สุดท้ายก็มีทุกอย่างที่อยู่ใน stage ที่มันสร้างจากมา ถ้าติดตั้ง toolchain กับ dev dependencies ไว้ใน stage นั้น ของพวกนี้จะค้างอยู่ใน image แม้ขั้นตอนหลังจะ build แอปเสร็จแล้วและไม่ได้ใช้มันอีก
Multi-stage build แก้เรื่องนี้ด้วยการแยก stage หนึ่งไว้ build และอีก stage ที่เล็กกว่าไว้รัน มีแค่ไฟล์ที่เราสั่ง COPY --from ข้ามมาเท่านั้นที่จะเข้า image สุดท้าย ส่วน build stage ทั้งหมดจะถูกทิ้งไป
ตอนนี้เราทำอย่างไร
ตอนนี้เรา build ใน stage แรก ตัด dev dependencies ออกที่ stage นั้น แล้วเริ่ม runtime stage จาก base image แบบ slim จากนั้น copy มาแค่ไฟล์ที่ build แล้ว, package manifest และ node_modules ที่เป็น production เท่านั้น image เลยลดจาก 1.2GB เหลือ 340MB หรือเล็กลง 71%
ผลพลอยได้คือพื้นที่ให้โจมตีน้อยลง แพ็กเกจใน image น้อยลง ก็มีของที่ต้องอัปเดตแพตช์น้อยลง และผลสแกนก็ไม่ขึ้นเรื่องที่ไม่เกี่ยวกับโค้ดเรา
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --omit=dev
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]เช็กลิสต์สั้น ๆ
- 1ตั้งชื่อ build stage ด้วย AS build แล้วติดตั้งกับ compile ที่นั่น
- 2รัน npm prune --omit=dev (หรือติดตั้งเฉพาะ production dependencies แยกต่างหาก) ตอนท้าย build stage
- 3เริ่ม FROM ใหม่ด้วย runtime image แบบ slim
- 4ใช้ COPY --from=build เฉพาะ output, manifest และ node_modules ที่เป็น production
- 5เทียบขนาดก่อนและหลังด้วย docker image ls
จำไว้สั้น ๆ
build ใน stage หนึ่ง รันใน stage ที่สอง แล้ว copy ข้ามไปเฉพาะสิ่งที่แอปต้องใช้ตอนรัน