Multi-Stage Builds
ปัญหาของ single-stage builds
หัวข้อที่มีชื่อว่า “ปัญหาของ single-stage builds”application ทั่วไปต้องการเครื่องมือที่แตกต่างกันมากในสองช่วง:
- ช่วง Build: compiler, test runner, type checker, build toolchain (
tsc,go build,mavenฯลฯ) - ช่วง Runtime: เฉพาะ output ที่ compile แล้วและ dependency ที่จำเป็นสำหรับการรัน
ใน Dockerfile แบบ single-stage ทั่วไป เราติดตั้งทุกอย่างใน image เดียว compiler ที่จำเป็นในช่วง build ตอนนี้นั่งว่างอยู่ใน production โดยเพิ่มขนาดหลายร้อย MB และ attack surface ที่ไม่จำเป็น
# Naive single-stage — compiler ends up in the final imageFROM node:22WORKDIR /appCOPY . .RUN npm ciRUN npm run buildCMD ["node", "dist/server.js"]ผลลัพธ์ที่พบบ่อย:
myapp:naive 1.21GBMulti-stage builds
หัวข้อที่มีชื่อว่า “Multi-stage builds”Docker BuildKit รองรับ multi-stage builds: Dockerfile เดียวที่มี instruction FROM หลายอัน แต่ละ FROM เริ่ม stage ใหม่ คุณสามารถ copy artifact จาก stage ก่อนหน้าไปยัง stage ถัดไปโดยใช้ COPY --from=<stage>
สิ่งสำคัญ: เฉพาะ stage สุดท้ายเท่านั้นที่จะถูก export เป็น final image stage กลางทั้งหมด รวมถึงทุก tool ที่ติดตั้งใน stage เหล่านั้น จะถูกทิ้งโดยอัตโนมัติ
โครงสร้างของ multi-stage Dockerfile
หัวข้อที่มีชื่อว่า “โครงสร้างของ multi-stage Dockerfile”# syntax=docker/dockerfile:1
# --- Stage 1: build ---FROM node:22-alpine AS buildWORKDIR /appCOPY package.json package-lock.json ./RUN npm ciCOPY src/ ./src/RUN npm run build # produces dist/
# --- Stage 2: runtime ---FROM node:22-alpine AS runtimeWORKDIR /appCOPY --from=build /app/dist ./distCOPY --from=build /app/node_modules ./node_modulesENV NODE_ENV=productionEXPOSE 3000CMD ["node", "dist/server.js"]สิ่งที่เปลี่ยนไป:
| Single-stage | Multi-stage | |
|---|---|---|
| Final image มี | Source + devDeps + dist | เฉพาะ dist + prodDeps |
| ขนาดโดยประมาณ (Node.js) | ~1.2 GB | ~110 MB |
| Build tools ใน prod | มี | ไม่มี |
การตั้งชื่อ stage
หัวข้อที่มีชื่อว่า “การตั้งชื่อ stage”ตั้งชื่อที่มีความหมายให้แต่ละ stage ด้วย AS <name> จากนั้นสามารถ reference ได้ใน COPY --from=<name> และยัง build ได้เฉพาะ stage ที่ต้องการระหว่างพัฒนา:
# Build เฉพาะ build stage (เหมาะสำหรับรัน test บน CI)docker build --target build -t myapp:ci .
# Build image เต็มสำหรับ productiondocker build -t myapp:prod .ตัวอย่าง Go
หัวข้อที่มีชื่อว่า “ตัวอย่าง Go”Go เหมาะอย่างยิ่งสำหรับ multi-stage builds: compiler สร้าง binary เดี่ยวแบบ static ที่ไม่ต้องการ runtime ใดๆ
# syntax=docker/dockerfile:1FROM golang:1.23-alpine AS buildWORKDIR /appCOPY go.mod go.sum ./RUN go mod downloadCOPY . .RUN CGO_ENABLED=0 go build -o server ./cmd/server
FROM scratch AS runtimeCOPY --from=build /app/server /serverEXPOSE 8080ENTRYPOINT ["/server"]FROM scratch คือ base ขั้นต่ำสุด — filesystem ว่างเปล่าสมบูรณ์ ไฟล์เดียวใน final image คือ binary ที่ compile แล้ว ผลลัพธ์มักจะต่ำกว่า 10 MB
ลงมือทำ: multi-stage Node.js build
หัวข้อที่มีชื่อว่า “ลงมือทำ: multi-stage Node.js build”snippet ด้านล่างรัน two-stage build ใน Play with Docker และแสดงขนาดของ final image
# syntax=docker/dockerfile:1
# Stage 1 — build
FROM node:22-alpine AS build
WORKDIR /app
RUN echo '{"name":"demo","version":"1.0.0","scripts":{"build":"echo built"}}' > package.json
RUN npm run build
RUN echo "console.log('Hello from optimized image!');" > dist/server.js
# Stage 2 — runtime (only dist/ lands here)
FROM node:22-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node","dist/server.js"]
# --- build & run ---
# docker build -t myapp:multi .
# docker run --rm myapp:multi
# docker images myapp:multiข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| ตัวเลือก | Benefit | Cost |
|---|---|---|
| Multi-stage build | final image เล็กมาก มีเฉพาะ runtime artifact ไม่มี build tool ตกค้าง | Dockerfile ซับซ้อนขึ้น ต้องดูแลสอง stage ขึ้นไปให้ sync กันตลอด |
| Runtime stage เป็น alpine | เล็ก มี shell ให้ exec เข้าไป debug ได้สะดวก | ยังมี package manager และ shell ซึ่งเพิ่ม attack surface เทียบกับ distroless |
| Runtime stage เป็น distroless | เล็กกว่า alpine อีกขั้น ไม่มี shell จึงปลอดภัยกว่า | ไม่มี shell ทำให้ exec เข้าไป debug container ที่กำลังรันแบบ interactive ไม่ได้เลย |
ข้อผิดพลาดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ข้อผิดพลาดที่พบบ่อย”- ใช้ base image เดียวกันทั้ง build stage และ runtime stage ทั้งที่ runtime ไม่ต้องการ compiler เลย ทำให้พลาดโอกาสลดขนาด image ไปมาก
COPY --from=<stage>ทั้งไดเรกทอรีของ build stage แทนที่จะเลือกเฉพาะไฟล์ artifact ที่จำเป็น ทำให้ final image ใหญ่เกินความจำเป็น- ไม่ pin version ของ base image ในแต่ละ stage (เช่นใช้
latest) ทำให้ build ไม่ reproducible ระหว่าง environment
💡 ตัวอย่างจากของจริง
Binary ที่ compile แบบ static ด้วย Go หรือ Rust แล้วรันบน
distrolessหรือscratchbase image มักได้ production image สุดท้ายต่ำกว่า 20MB เป็นประจำ ที่เป็น pattern ที่ Google แนะนำและ publish เองผ่านgcr.io/distroless