Docker Multi-stage Build: ลดขนาด Image จาก 1.2GB เหลือ 90MB
· 2 min read
Image ที่ใหญ่เกินจำเป็นทำให้ deploy ช้า ค่า egress แพง และมี attack surface มากกว่าที่ควร วิธีแก้ที่ได้ผลที่สุดคือ multi-stage build ซึ่งแยก "ของที่ใช้ตอน build" ออกจาก "ของที่ต้องมีตอนรัน"
ปัญหาของ Dockerfile แบบ stage เดียว
FROM node:22
WORKDIR /app
COPY . .
RUN npm install && npm run build
CMD ["npm", "start"]Image ที่ได้มีทั้ง devDependencies, source code, cache ของ package manager และ toolchain สำหรับ compile ทั้งหมดนี้ไม่มีอะไรถูกใช้ตอน runtime เลย
แยกเป็นสอง stage
FROM node:22-slim AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build && pnpm prune --prod
FROM node:22-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next ./.next
COPY --from=builder /app/public ./public
USER node
CMD ["node_modules/.bin/next", "start"]จุดสำคัญคือ stage สุดท้าย copy เฉพาะผลลัพธ์ที่ต้องใช้ ส่วน compiler และ cache ทั้งหมดถูกทิ้งไปพร้อม stage แรก
จัดลำดับ layer ให้ cache ทำงาน
Docker cache ทำงานตามลำดับคำสั่ง ถ้า COPY . . มาก่อน install ทุกครั้งที่แก้โค้ดแม้แต่บรรทัดเดียว layer install จะถูกสร้างใหม่หมด ให้ copy เฉพาะไฟล์ lock ก่อนเสมอ แล้วค่อย copy source ทีหลัง
เลือก base image ตามข้อจำกัดจริง
alpine เล็กที่สุดแต่ใช้ musl ซึ่งทำให้ native module บางตัวต้อง compile ใหม่และเจอ bug แปลกๆ ส่วน slim ใหญ่กว่าเล็กน้อยแต่เป็น glibc เหมือน environment ส่วนใหญ่ ถ้าไม่ได้จำเป็นต้องบีบทุกเมกะไบต์ slim มักคุ้มกว่าเวลาที่เสียไปกับการไล่ปัญหา
ตรวจว่าอะไรกินพื้นที่
docker history --no-trunc my-app:latest
docker run --rm -it my-app:latest du -sh /app/* | sort -hดู layer ทีละชั้นก่อนจะเดา เพราะตัวการมักเป็นของที่ไม่คิดว่าติดมา เช่นโฟลเดอร์ .git หรือ test fixture ที่ลืมใส่ใน .dockerignore