Docker Optimization and Multi-stage Builds
Back to Infrastructure Index
The Problem: Docker Image Bloating
ปัญหาการ “บวม” (Docker image bloating) หรือก็คือ image มีขนาดใหญ่เกินความจำเป็นนั้น สามารถเกิดขึ้นได้ถ้าไม่จัดการให้ดี
Why Does it Matter?
- Slower deployment - Large images take longer to push/pull
- More storage costs - Consumes more disk space
- Security risks - More packages = more potential vulnerabilities
- Network bandwidth - Wastes bandwidth during transfers
Optimization Techniques
1. Use .dockerignore
เพื่อไม่ให้เพิ่มไฟล์ที่ไม่ต้องการ
# Development files
node_modules/
.git/
.env
*.md
# Build artifacts
*.log
dist/
coverage/2. Choose Minimal Base Images
เลือกใช้ distroless/minimal base image เพื่อลดขนาดให้ได้มากที่สุด
# Instead of
FROM ubuntu:20.04
# Use
FROM alpine:latest
# or
FROM gcr.io/distroless/base3. Multi-stage Builds ⭐
Multi-Stage Builds
Multi-stage builds จะช่วยลดขนาดของ container ให้เล็กลงได้ ด้วยการแตกขั้นตอนออกเป็นหลายๆ stage ย่อย แต่ละ stage จะส่งผลลัพธ์ต่อไปยัง stage ถัดไป
Key Concept
ต้องเข้าใจว่าส่วนที่ใช้ทำในตอน build แอปพลิเคชันนั้น พอเสร็จแล้วมักจะไม่ถูกนำมาใช้ต่อตอนที่แอปกำลังรัน ถ้าเรานำทั้งหมดมาใช้ก็จะทำให้บวมได้
Solution: แยกส่วนที่เป็น build tool ออก ให้เหลือเฉพาะส่วนที่จำเป็นต่อการรันแอปนั่นเอง
Practical Example: Go Application
Demo repository: GitHub - nattrio/goimdb
❌ Single-stage Build (Not Optimized)
FROM golang:1.18-alpine
WORKDIR /crud
COPY . .
RUN go mod download
EXPOSE 2565
RUN go build -o /test main.go
CMD [ "/test" ]Result: ~430 MB
✅ Multi-stage Build (Optimized)
# Build stage
FROM golang:1.18-alpine AS builder
WORKDIR /crud
COPY . .
RUN go mod download
EXPOSE 2565
RUN go build -o /test main.go
# Deploy stage
FROM alpine:latest
WORKDIR /
COPY --from=builder /test /test
EXPOSE 2565
USER nonroot:nonroot
ENTRYPOINT [ "/test" ]Result: ~16 MB 🎉
Comparison Results

จะเห็นว่าการใช้ Multi-stage builds ช่วยลดขนาดให้ image ของเราอย่างมหาศาลเทียบกับแบบปกติ จาก 430 MB เป็น 16 MB พอเล็กแล้วก็สามารถนำไปใช้ต่อได้ง่ายมากขึ้นนั่นเอง
Benefits of Multi-stage Builds
- Smaller Image Size - Only production artifacts included
- Better Security - Fewer components = reduced attack surface
- Faster Deployment - Smaller images transfer faster
- Cleaner Separation - Build vs Runtime environments clearly separated
- Cost Effective - Less storage and bandwidth costs
Best Practices
1. Name Your Stages
FROM golang:1.18 AS builder
FROM node:16 AS frontend-builder
FROM alpine:latest AS production2. Copy Only What’s Needed
# Copy only the binary, not everything
COPY --from=builder /app/binary /app/binary3. Use Specific Tags
# Instead of
FROM golang:latest
# Use
FROM golang:1.18-alpine4. Leverage Build Cache
# Copy go.mod first to leverage cache
COPY go.mod go.sum ./
RUN go mod download
# Then copy source code
COPY . .
RUN go build5. Run as Non-root User
USER nonroot:nonroot
# or
RUN addgroup -g 1000 app && adduser -u 1000 -G app -s /bin/sh -D app
USER appMulti-stage Build Pattern
# Stage 1: Build
FROM <build-image> AS builder
WORKDIR /build
COPY . .
RUN <build-commands>
# Stage 2: Runtime
FROM <minimal-runtime-image>
WORKDIR /app
COPY --from=builder /build/artifact ./
EXPOSE <port>
CMD ["./artifact"]Related:
Reference: