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/base

3. 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

  1. Smaller Image Size - Only production artifacts included
  2. Better Security - Fewer components = reduced attack surface
  3. Faster Deployment - Smaller images transfer faster
  4. Cleaner Separation - Build vs Runtime environments clearly separated
  5. 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 production

2. Copy Only What’s Needed

# Copy only the binary, not everything
COPY --from=builder /app/binary /app/binary

3. Use Specific Tags

# Instead of
FROM golang:latest
 
# Use
FROM golang:1.18-alpine

4. 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 build

5. 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 app

Multi-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: