Docker Multi-stage Builds — mình đã giảm 80% dung lượng image như thế nào

Phong

Hồi mới bắt đầu deploy ứng dụng Go lên server, mình nhớ cái cảm giác vừa build xong image là nhìn dòng "Sending build context to Docker daemon 812MB" mà muốn xỉu luôn. 800MB cho một cái API nhỏ xíu? Thiệt tình là bất hợp lý quá trời.

Docker containers network Ảnh: Wolfgang Weiser — Pexels

Lúc đó mình dùng kiểu Dockerfile truyền thống: lấy image Go SDK, copy code vô, chạy go build, xong. Cơ mà image kết quả nặng hơn mình tưởng rất nhiều, vì nó mang luôn cả Go toolchain, source code với đống thư viện compile-time không cần thiết lúc chạy.

Rồi mình bắt đầu mày mò. Đọc docs thì thấy Docker có cái gọi là multi-stage builds — đơn giản là dùng nhiều FROM trong cùng một Dockerfile, mỗi stage lo một việc khác nhau. Stage đầu dùng để build (builder stage), stage sau copy binary từ stage trước rồi dùng base image nhẹ hơn nhiều.

# Stage 1: Build
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 myapp .

# Stage 2: Run — siêu nhẹ
FROM alpine:3.19
COPY --from=builder /app/myapp /myapp
EXPOSE 8080
CMD ["/myapp"]

Kết quả thì khỏi bàn: image cuối cùng từ 800MB+ xuống còn chưa tới 20MB. Tại sao? Vì stage cuối chỉ chứa đúng cái binary với Alpine image siêu nhẹ — không Go SDK, không source code, không package manager gì hết. Sạch như tờ giấy trắng.

Developer working on code Ảnh: Erik Mclean — Pexels

Từ hồi đó tới giờ, mình áp dụng multi-stage builds cho gần như mọi project — từ Go, Node.js cho tới .NET. Lần nào giảm được vài trăm MB cũng thấy nhẹ cả người. Deploy nhanh hơn, tốn ít bandwidth hơn, và bề mặt tấn công cũng nhỏ hơn vì không còn mấy thứ linh tinh trong image.

Một tip nhỏ: nếu dùng Alpine base image, nhớ thêm ca-certificates với tzdata vô — chứ không sáng hôm sau dậy thấy API báo lỗi SSL thì cũng mệt. À, với để ý cái CGO_ENABLED=0 ở trên — nếu không tắt CGO, Go build sẽ link với thư viện C, mà Alpine thì không có glibc, thành ra binary không chạy được.

Nói chung, multi-stage builds là một trong những cái hay ho nhất mình học được hồi mới chơi Docker. Nếu bạn chưa dùng thử, hãy thử ngay — đảm bảo thấy image của mình nhẹ hẳn.

📋 Phụ lục thuật ngữ

  • Multi-stage build — kỹ thuật dùng nhiều FROM trong Dockerfile để tách biệt môi trường build và runtime, giúp image cuối nhẹ và sạch hơn
  • Builder pattern — stage đầu chuyên để build code (thường dùng image SDK đầy đủ), stage sau chỉ copy artifact cần thiết
  • Alpine Linux — bản phân phối Linux siêu nhẹ (~5MB), phổ biến làm base image cho Docker runtime