ai

Docker Multi-stage Build คู่มือสมบูรณ์ — ลดขนาด

Docker Multi-stage Build คู่มือสมบูรณ์ — ลดขนาด

Docker Multi-stage Build คู่มือสมบูรณ์ เป็นหัวข้อที่ได้รับความสนใจอย่างมากในวงการ IT บทความนี้อธิบายหลักการทำงาน วิธีติดตั้ง และ best practices สำหรับการใช้งานจริง

Multi-stage Build คืออะไร

Docker Multi-stage Build คู่มือสมบูรณ์ — ลดขนาด

Docker Multi-stage Build คือเทคนิคการเขียน Dockerfile ที่อนุญาตให้ใช้ หลาย FROM statement ในไฟล์เดียวแต่ละ FROM จะสร้าง stage ใหม่ขึ้นมาโดย stage แรกๆทำหน้าที่ compile หรือ build application ส่วน stage สุดท้ายจะเป็น production image ที่เก็บเฉพาะ binary หรือ artifact ที่จำเป็นเท่านั้น

แนวคิดนี้ถูกเพิ่มเข้ามาตั้งแต่ Docker 17.05 (ปี 2017) และกลายเป็น standard practice สำหรับ production workload ทุกชนิดในปัจจุบันเพราะช่วยลดขนาด image ได้ 50-95% โดยไม่ต้องเปลี่ยน workflow การ build แต่อย่างใด

ตัวอย่างเปรียบเทียบขนาด image:

ApplicationSingle-stageMulti-stageลดลง
Go REST API1.2 GB12 MB99%
Node.js Express950 MB150 MB84%
Python FastAPI1.1 GB180 MB83%
Java Spring Boot680 MB200 MB70%
React Frontend1.5 GB25 MB98%

ปัญหาของ Single-stage Build

ก่อนจะมี multi-stage build นักพัฒนามักเขียน Dockerfile แบบ single-stage ซึ่งหมายความว่า ทุกอย่างอยู่ใน image เดียว ตั้งแต่ compiler, build tools, source code, test framework ไปจนถึง production binary สิ่งที่ตามมาคือปัญหาหลายด้าน:

  • ขนาด Image ใหญ่เกินจำเป็น — image ที่มี gcc, make, npm devDependencies รวมอยู่ด้วยอาจมีขนาด 1-2 GB ทั้งที่ application จริงๆต้องการแค่ไม่กี่สิบ MB
  • Attack Surface กว้าง — ยิ่ง image มี package มากยิ่งมีจุดที่อาจเกิดช่องโหว่ด้านความปลอดภัย CVE (Common Vulnerabilities and Exposures) ที่ scanner ตรวจพบจะมากตามจำนวน package
  • Build Cache ไม่มีประสิทธิภาพ — เมื่อเปลี่ยน source code แม้แค่บรรทัดเดียว Docker อาจต้อง rebuild layer ทั้งหมดที่อยู่หลังจุดที่เปลี่ยนทำให้ build ช้า
  • Deploy ช้า — ใน Kubernetes cluster ที่มีหลายร้อย node ทุก node ต้อง pull image ใหม่เมื่อ deploy ถ้า image ขนาด 1.5 GB กับ 50 MB เวลาที่ใช้ต่างกันมหาศาล
  • Secret Leak — API key หรือ credential ที่ใช้ตอน build อาจติดค้างอยู่ใน layer ของ image ทำให้ผู้ที่เข้าถึง image สามารถดึง secret ออกมาได้

โครงสร้าง Multi-stage Dockerfile

โครงสร้างพื้นฐานของ multi-stage Dockerfile ประกอบด้วย 2 ส่วนหลัก:

เนื้อหาเกี่ยวข้อง — บทความที่เกี่ยวข้อง: email hosting คือ

# === Stage 1: Builder ===

FROM golang:1.22-alpine AS builder

WORKDIR /app

COPY go.mod go.sum ./

RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server



# === Stage 2: Production ===

FROM alpine:3.19

RUN apk --no-cache add ca-certificates tzdata

COPY --from=builder /app/server /usr/local/bin/server

EXPOSE 8080

CMD ["server"]

คำสั่งที่สำคัญที่สุดคือ COPY --from=builder ซึ่งเป็นการ คัดลอก artifact จาก stage ก่อนหน้า มาใส่ใน stage ปัจจุบันโดย stage builder จะถูก discard ไปหลัง build เสร็จไม่ถูกรวมเข้าไปใน final image

คุณสามารถมีกี่ stage ก็ได้ตามต้องการเช่น stage สำหรับ test, stage สำหรับ lint, stage สำหรับ build static assets แยกจาก backend เป็นต้น

แนะนำเพิ่มเติม — ติดตาม XM Signal

ตัวอย่างจริง: Go Application

Go เป็นภาษาที่ได้ประโยชน์จาก multi-stage build มากที่สุดเพราะ compile ออกมาเป็น static binary ไม่ต้องพึ่ง runtime ภายนอกสามารถรันบน scratch หรือ distroless image ได้เลย

# Build stage

FROM golang:1.22-alpine AS build

WORKDIR /src

COPY go.mod go.sum ./

RUN go mod download

COPY . .

RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /bin/api ./cmd/api



# Production stage - ใช้ scratch (0 bytes base)

FROM scratch

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

COPY --from=build /bin/api /api

EXPOSE 8080

ENTRYPOINT ["/api"]

ผลลัพธ์: image สุดท้ายมีขนาดเพียง 8-15 MB เทียบกับ golang:1.22-alpine ที่มีขนาด 250 MB+ และ flag -ldflags="-s -w" ช่วยลดขนาด binary โดยตัด debug information ออก

ตัวอย่างจริง: Node.js Application

สำหรับ Node.js จะแตกต่างจาก Go ตรงที่ต้องมี runtime (Node) อยู่ใน final image ดังนั้นเราจะใช้ multi-stage เพื่อ แยก build dependencies ออกจาก production dependencies

เนื้อหาเกี่ยวข้อง — ดูเพิ่มเติมเรื่อง Incident.io Shift Left Security

# Stage 1: Install ALL dependencies และ build

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci

COPY . .

RUN npm run build

RUN npm prune --production



# Stage 2: Production - เฉพาะ production deps

FROM node:20-alpine

WORKDIR /app

RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -s /bin/sh -D appuser

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

COPY --from=builder /app/package.json ./

USER appuser

EXPOSE 3000

CMD ["node", "dist/index.js"]

จุดสำคัญ: npm prune --production จะลบ devDependencies ทิ้งและเราสร้าง non-root user ด้วย adduser เพื่อความปลอดภัย image สุดท้ายจะมีเฉพาะ compiled code กับ production dependencies เท่านั้น

ตัวอย่างจริง: Python FastAPI

Python ใช้ multi-stage ได้ดีมากโดยเฉพาะเมื่อต้อง compile C extensions ใน stage แรกแล้วคัดลอกเฉพาะ wheel ที่ build เสร็จแล้วไป stage สุดท้าย

# Stage 1: Build wheels

FROM python:3.12-slim AS builder

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends gcc libpq-dev

COPY requirements.txt .

RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt



# Stage 2: Production

FROM python:3.12-slim

WORKDIR /app

COPY --from=builder /wheels /wheels

RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels

COPY . .

RUN useradd -r -s /bin/false appuser && chown -R appuser:appuser /app

USER appuser

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

เทคนิคสำคัญคือการใช้ pip wheel ใน build stage เพื่อ pre-compile ทุก package เป็น wheel file จากนั้น pip install จาก wheel ใน production stage โดยไม่ต้องมี compiler ทำให้ final image ไม่มี gcc, libpq-dev หรือ header files ใดๆ

ตัวอย่างจริง: Java Spring Boot

Java application มักมีขนาดใหญ่เพราะต้องรวม JDK ไว้ด้วยแต่ด้วย multi-stage เราสามารถใช้ JDK สำหรับ build แต่ใช้ JRE สำหรับ run ได้

แนะนำเพิ่มเติม — อ่านเพิ่มเติมที่ SiamCafeBook

# Stage 1: Build with Maven

FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app

COPY pom.xml .

RUN mvn dependency:go-offline -B

COPY src ./src

RUN mvn package -DskipTests -B



# Stage 2: Run with JRE only

FROM eclipse-temurin:21-jre-alpine

WORKDIR /app

RUN addgroup -S spring && adduser -S spring -G spring

COPY --from=builder /app/target/*.jar app.jar

USER spring

EXPOSE 8080

ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

JDK มีขนาดประมาณ 400 MB แต่ JRE Alpine มีขนาดเพียง 130 MB และ flag -XX:+UseContainerSupport ทำให้ JVM รู้จัก container memory limit จึงจัดการ heap ได้อย่างเหมาะสม

เทคนิค Advanced — Named Stages และ Build Arguments

นอกจากรูปแบบพื้นฐาน 2 stage แล้ว Docker multi-stage ยังรองรับเทคนิค advanced หลายอย่าง:

เนื้อหาเกี่ยวข้อง — อ่านต่อ: Airbyte ETL Post-mortem Analysis —

Named Stages และ Target Build

Docker Multi-stage Build คู่มือสมบูรณ์ — ลดขนาด
# Dockerfile with named stages

FROM node:20-alpine AS deps

WORKDIR /app

COPY package*.json ./

RUN npm ci



FROM deps AS test

COPY . .

RUN npm test



FROM deps AS build

COPY . .

RUN npm run build



FROM nginx:alpine AS production

COPY --from=build /app/dist /usr/share/nginx/html

คุณสามารถ build เฉพาะ stage ที่ต้องการด้วย docker build --target test . ซึ่งมีประโยชน์มากใน CI/CD pipeline เพราะแยก test กับ build ออกจากกันได้

Build Arguments สำหรับ Dynamic Base Image

ARG NODE_VERSION=20

FROM node:-alpine AS builder

ARG BUILD_ENV=production

ENV NODE_ENV=

WORKDIR /app

COPY . .

RUN npm ci && npm run build

การใช้ ARG ทำให้ Dockerfile เดียวรองรับได้หลาย version และหลาย environment โดยเปลี่ยนค่าตอน build ด้วย docker build --build-arg NODE_VERSION=18 .

Multi-stage กับ CI/CD Pipeline

Multi-stage build ทำงานร่วมกับ CI/CD ได้อย่างยอดเยี่ยมตัวอย่างการใช้กับ GitHub Actions:

# .github/workflows/build.yml

name: Build and Push

on: [push]

jobs:

 build:

 runs-on: ubuntu-latest

 steps:

 - uses: actions/checkout@v4

 - uses: docker/setup-buildx-action@v3

 - uses: docker/build-push-action@v5

 with:

 context: .

 target: production

 push: true

 tags: ghcr.io/myorg/myapp:}

 cache-from: type=gha

 cache-to: type=gha, mode=max

สิ่งที่ควรทำใน CI/CD pipeline:

  • ใช้ BuildKit cachecache-from และ cache-to ช่วยให้ build ครั้งถัดไปเร็วขึ้น 3-5 เท่าเพราะ reuse layer จาก build ก่อนหน้า
  • แยก test stage — รัน test ใน stage แยกถ้า test fail จะไม่ build production image
  • Scan image — ใช้เครื่องมือเช่น Trivy หรือ Snyk scan final image เพื่อหา vulnerability ก่อน push ไป registry
  • Tag ด้วย git SHA — ทำให้ trace ได้ว่า image มาจาก commit ไหนสะดวกต่อการ rollback

Best Practices 10 ข้อ

  1. ใช้ specific tag แทน latest — เช่น node:20.11-alpine แทน node:latest เพื่อให้ build reproducible ทุกครั้ง
  2. COPY dependency file ก่อน source — เช่น COPY package.json ก่อน COPY . เพื่อใช้ประโยชน์จาก Docker layer cache ถ้า dependency ไม่เปลี่ยนจะไม่ต้อง install ใหม่
  3. ใช้ .dockerignore — ป้องกันไม่ให้ .git, node_modules, .env ถูก COPY เข้า build context ช่วยให้ build เร็วขึ้นและปลอดภัยขึ้น
  4. รัน container ด้วย non-root user — สร้าง user ด้วย adduser และใช้ USER instruction เพื่อลด attack surface
  5. ใช้ HEALTHCHECK — เพิ่ม HEALTHCHECK instruction เพื่อให้ Docker และ orchestrator ตรวจสอบ health ของ container ได้
  6. Minimize layer count — รวม RUN command ที่เกี่ยวข้องกันด้วย && เพื่อลดจำนวน layer
  7. ใช้ alpine หรือ distroless — เลือก base image ที่เล็กที่สุดเท่าที่ application ต้องการ
  8. อย่าเก็บ secret ใน image — ใช้ Docker BuildKit secret mount (--mount=type=secret) แทนการ COPY secret file เข้าไป
  9. Set metadata ด้วย LABEL — เพิ่ม LABEL เช่น maintainer, version, description เพื่อให้ manage image ได้ง่าย
  10. ใช้ multi-platform build — ใช้ docker buildx build --platform linux/amd64, linux/arm64 เพื่อรองรับทั้ง x86 และ ARM architecture

เปรียบเทียบ Base Image ยอดนิยม

การเลือก base image สำหรับ final stage มีผลต่อทั้งขนาดและความปลอดภัย:

เนื้อหาเกี่ยวข้อง — อ่านต่อ: PagerDuty Incident Technical Debt Management

Base ImageขนาดPackage Managerเหมาะกับ
scratch0 MBไม่มีGo, Rust (static binary)
alpine:3.197 MBapkทุกภาษายอดนิยมที่สุด
distroless2-20 MBไม่มีJava, Python, Node.js
debian-slim80 MBaptApplication ที่ต้องการ glibc
ubuntu78 MBaptDevelopment, debugging

คำแนะนำ: เริ่มจาก alpine หรือ distroless ก่อนถ้า application มีปัญหากับ musl libc (ซึ่ง alpine ใช้) ค่อยเปลี่ยนไปใช้ debian-slim ที่ใช้ glibc แทน

Debugging Multi-stage Build

เมื่อ multi-stage build มีปัญหามีเทคนิคหลายอย่างที่ช่วยแก้ debug ได้:

Build เฉพาะ Stage ที่ต้องการ

# Build แค่ builder stage เพื่อตรวจสอบ

docker build --target builder -t myapp:debug .



# เข้าไป shell เพื่อ debug

docker run -it myapp:debug /bin/sh

ดู Layer และ Size ทีละ Stage

# ดูขนาดแต่ละ layer

docker history myapp:latest



# ใช้ dive เพื่อดูรายละเอียด layer

dive myapp:latest

ตรวจสอบ Build Cache

# ดู build cache usage

docker builder prune --dry-run



# Force rebuild โดยไม่ใช้ cache

docker build --no-cache .

เครื่องมือ dive เป็น open source tool ที่แสดง layer ของ image เป็น interactive UI ช่วยให้เห็นว่าแต่ละ layer เพิ่มไฟล์อะไรบ้างและมีไฟล์ไหนที่ไม่จำเป็น

Multi-stage build ช้ากว่า single-stage build หรือไม่?

ไม่จำเป็นใน build แรกอาจใช้เวลาพอๆกันแต่ build ครั้งถัดไปจะเร็วกว่ามากเพราะ Docker cache แต่ละ stage แยกกันถ้า dependency ไม่เปลี่ยน stage ที่ install dependency จะถูก cache ไว้ทั้งหมดและ image ที่เล็กกว่าจะ push/pull เร็วกว่าหลายเท่า

XM Legend · เทรดเดอร์ & ผู้สอน Forex 13 ปี

ผู้ก่อตั้ง SiamCafe ตั้งแต่ปี 1997 · เทรดเดอร์สาย Forex มากกว่า 13 ปี ได้รับการยกย่องเป็น XM Legend · แบ่งปันความรู้ Forex, ไอที, AI และการเทรด จากประสบการณ์จริงในตลาดจริง