ข้ามไปยังเนื้อหา

Docker Multi-Stage Builds

เมื่อคุณ containerise แอปพลิเคชัน Node/TypeScript image สุดท้ายต้องมี:

  1. Node.js runtime เต็มรูปแบบ (100+ MB)
  2. node_modules/ (สำหรับ NestJS app ทั่วไปอาจถึง 300–600 MB)
  3. output .js ที่คอมไพล์แล้ว

ผลลัพธ์มักเป็น image ขนาด 500 MB–1 GB ที่ขน development dependency tree ทั้งก้อนขึ้น production คุณลดขนาดได้ด้วย .dockerignore, --omit=dev และ multi-stage builds แต่ตัว Node runtime เองไม่มีทางหายไป

Go คอมไพล์ออกมาเป็น binary เดียวแบบ statically-linked ที่ไม่ต้องพึ่ง runtime ใด ๆ image สุดท้ายจึง build จาก FROM scratch — base image ที่ว่างเปล่าจริง ๆ — โดยมีแค่ binary ของคุณอยู่ข้างในได้เลย ขนาด production image ของ Go จึงอยู่ราว 5–20 MB

Multi-stage build ใช้ Go builder image เต็มรูปแบบในการคอมไพล์ แล้ว copy เฉพาะ binary ที่ได้ไปยัง final image ขนาดเล็ก

# ---- Stage 1: Build ----
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Copy dependency manifests ก่อนเพื่อ layer caching
COPY go.mod go.sum ./
RUN go mod download
# Copy source และ build static binary
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# ---- Stage 2: Run ----
FROM gcr.io/distroless/static-debian12
WORKDIR /app
# Copy เฉพาะ binary จาก builder stage
COPY --from=builder /app/server .
EXPOSE 8080
ENTRYPOINT ["/app/server"]

CGO_ENABLED=0 ปิดการใช้ C bindings เพื่อให้ binary เป็น self-contained อย่างสมบูรณ์ -ldflags="-s -w" strip debug symbols ลดขนาด binary ประมาณ ~30% distroless/static คือ minimal image ที่ Google ดูแลซึ่งมีเฉพาะ CA certificates และ timezone data — มีประโยชน์สำหรับ HTTPS calls — แต่ไม่มี shell, ไม่มี package manager, ไม่มี attack surface

ถ้าต้องการ shell สำหรับ debug ให้เปลี่ยน distroless/static-debian12 เป็น alpine:3.20 (ยังแค่ ~7 MB) และเพิ่ม apk add --no-cache ca-certificates

# ---- Stage 1: Build ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build # tsc output → dist/
# Prune dev dependencies
RUN npm prune --omit=dev
# ---- Stage 2: Run ----
FROM node:20-alpine
WORKDIR /app
# ต้อง copy node_modules — runtime ยังต้องการมัน
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json .
EXPOSE 3000
CMD ["node", "dist/main.js"]

แม้หลัง prune dev deps แล้ว Node runtime และ node_modules ที่เหลือทำให้ image ยังมีขนาด 150–300 MB

TypeScript
# ขนาด Node image สุดท้าย: ~150-300 MB
# มี: node runtime + node_modules + dist/
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/main.js"]
Go
# ขนาด Go image สุดท้าย: ~5-20 MB
# มี: binary เท่านั้น (ไม่มี runtime)
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 -ldflags="-s -w" \
-o /app/server ./cmd/server
FROM gcr.io/distroless/static-debian12
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
ENTRYPOINT ["/app/server"]

เหล่านี้คือตัวอย่าง Dockerfile — รันใน terminal ด้วย docker build -t myapp . แล้วใช้ docker images myapp เพื่อเปรียบเทียบขนาด ไม่มี browser playground สำหรับส่วนนี้

`CGO_ENABLED=0` ทำอะไรเมื่อ build Go binary สำหรับ Docker?
ทำไม Go Docker image ถึงใช้ `FROM scratch` ได้ แต่ Node image ทำไม่ได้?
`-ldflags="-s -w"` ใน Go build command มีจุดประสงค์อะไร?
Go production image ไหนที่แนะนำสำหรับ service ที่ต้องการ CA certificates?