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.