Hồi mới học Docker, mình build image Go xong thấy nó nặng tận 1GB. Deploy lên server chậm rì, tốn bandwidth, mà trong image toàn những thứ không cần thiết: Go compiler, hệ thống package, với đống cache.

Rồi mình biết đến multi-stage builds — và từ đó đến giờ chưa bao giờ viết Dockerfile kiểu cũ nữa.


Vấn đề: Image "béo" đến từ đâu?

Hồi đó Dockerfile của mình đơn giản thế này:

FROM golang:1.23
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server .
CMD ["./server"]

Image nặng 800MB–1.2GB. Trong đó chứa cả Go toolchain, compiler, thư viện hệ thống, source code — những thứ runtime không cần. Bản chất, ứng dụng Go chỉ cần static binary để chạy, chẳng cần gì thêm.


Multi-stage builds — giải pháp đơn giản mà hiệu quả

Ý tưởng: dùng nhiều lệnh FROM trong một Dockerfile. Stage đầu (builder) chứa toàn bộ công cụ build. Stage cuối (runtime) chỉ copy đúng binary vào base image nhẹ nhất có thể.

# === STAGE 1: Builder ===
FROM golang:1.23 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app/server .

# === STAGE 2: Runtime ===
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Image lúc này chỉ còn 20–30MB — giảm 95–97%. Deploy nhanh hơn, lưu trữ rẻ hơn, bảo mật hơn.


Tại sao Go lại hợp với multi-stage đến vậy?

Vì Go compile thành static binary. Khi bạn tắt CGO (CGO_ENABLED=0) và strip debug info (-ldflags="-s -w"), binary Go hoàn toàn tự thân — không phụ thuộc thư viện C hay shared library nào. Nó chỉ cần kernel Linux là chạy được.

Đây là lý do Go là ngôn ngữ số một cho container: bạn có thể dùng base image cực kỳ nhỏ.

So sánh base image

Base Image Size Bảo mật Khi nào dùng
golang:1.23 ~800MB Thấp Chỉ dùng để build
alpine:3.20 ~5MB Trung bình Đa số trường hợp
distroless ~15MB Cao (không shell) Production cần security
scratch 0MB Cao nhất Binary không cần gì thêm

Ví dụ với scratch image:

FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Image chỉ 15–20MB — chỉ gồm binary và kernel syscalls.


Kinh nghiệm thực tế trong production

1. Cache layer cho go mod

Đặt go mod download trước khi copy source code:

COPY go.mod go.sum ./
RUN go mod download    # ← cache layer này nếu go.mod không đổi
COPY . .               # ← chỉ build lại khi code thay đổi

Docker cache từng layer — nếu không thay đổi go.mod, layer này được reuse, build nhanh hơn hẳn.

2. Distroless images

Google cung cấp image distroless — chỉ có binary + runtime tối thiểu, không có shell, không package manager, không lỗ hổng leo thang đặc quyền:

FROM gcr.io/distroless/base-debian12:latest
COPY --from=builder /app/server /server
CMD ["/server"]

Cực kỳ an toàn cho production — CVE scan hầu như zero.

3. Dockerignore

Đừng quên .dockerignore để context nhẹ:

.git/
node_modules/
*.md
.env
*.test.go

Build nhanh hơn 30–50% chỉ nhờ cái này.

4. Build nhiều binary

Một Dockerfile build cả server lẫn worker:

FROM golang:1.23 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /server ./cmd/server
RUN CGO_ENABLED=0 go build -o /worker ./cmd/worker

FROM alpine:3.20
COPY --from=builder /server /server
COPY --from=builder /worker /worker

Kết luận

Multi-stage builds là kỹ thuật đơn giản nhưng cực kỳ hiệu quả. Với Go, bạn có thể đưa image từ 1GB xuống còn 20MB — con số biết nói.

Những lợi ích bạn nhận được:

  • Deploy nhanh hơn — image nhỏ, pull và start trong giây lát
  • Bảo mật hơn — bề mặt tấn công giảm mạnh
  • Tiết kiệm chi phí — ít storage, ít bandwidth, ít thời gian CI/CD
  • Dễ maintain — một Dockerfile cho cả build và runtime

Nếu bạn chưa chuyển sang multi-stage, hãy thử ngay hôm nay — chỉ mất 5 phút sửa Dockerfile mà lợi ích kéo dài mãi.

Xem các bài viết khác