Docker multi-stage build — Cắt image từ 1.2GB xuống 90MB

Phong Hy

💡 Vấn đề

Hồi mới deploy service đầu tiên lên VPS, mình build Docker image xong gõ docker images thấy nặng 1.2GB. Máy local thì khỏe, nhưng pull về VPS — Oracle Free Tier chỉ có 1GB RAM — là khóc. Image nặng không chỉ tốn disk mà còn tốn băng thông mỗi lần deploy, CI chạy lâu hơn, attack surface cũng to hơn.

🔍 Vì sao image nặng?

Dockerfile kiểu đơn giản ai cũng viết lần đầu:

FROM golang:1.24
WORKDIR /app
COPY . .
RUN go build -o server .
CMD ["./server"]

Image này chứa cả toolchain Go — trình biên dịch, thư viện chuẩn, git, gcc… — mấy thứ đó chẳng liên quan gì lúc runtime. Trong khi binary Go đã là static, chạy được trên image trần như alpine hay thậm chí scratch.

🧪 Multi-stage build

Ý tưởng: dùng 1 image builder để compile, rồi copy chỉ mỗi binary sang image runtime sạch.

# Stage 1 — build
FROM golang:1.24-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 server .

# Stage 2 — runtime
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
COPY --from=builder /app/server .
EXPOSE 8080
USER 10001
ENTRYPOINT ["./server"]

Mấy trick quan trọng:

  • CGO_ENABLED=0 — build binary static, không phụ thuộc libc → chạy được trên image trần
  • -ldflags="-s -w" — strip debug info, cắt thêm ~30% kích thước
  • Copy go.mod go.sum trước rồi go mod download — tận dụng layer cache, sửa code không phải tải lại dependency
  • USER 10001 — chạy non-root, an toàn hơn hẳn

Kết quả: 1.2GB → ~90MB (alpine), hoặc ~15MB nếu dùng scratch vì binary Go thuần static không cần gì cả. Nhưng scratch không có ca-certificates — nếu service gọi HTTPS ra ngoài thì phải copy cert từ builder stage qua.

📝 Kinh nghiệm thực tế

  • VPS 1GB RAM: image nhỏ = pull nhanh, container khởi động gần như tức thì, không lo disk đầy
  • CI/CD: image nhỏ hơn 10 lần = pipeline nhanh hơn hẳn, chi phí lưu trữ registry giảm
  • Debug: đừng xoá stage builder vội — chạy docker build --target builder để có image đầy đủ tool khi cần exec vào sửa
  • Lỗi hay gặp: nếu dùng scratch mà gặp x509: certificate signed by unknown authority, nhớ copy ca-certificates.crt từ builder sang

Multi-stage là skill một lần học, xài mãi — áp dụng được cho Go, Rust, Node (prune devDeps trước khi copy), Java (chỉ copy JRE). Bắt đầu từ bài toán image nặng, bạn sẽ tự nhiên học thêm layer cache, security và hiểu Dockerfile sâu hơn.