Docker Multi-Stage Builds — Từ 1.2GB xuống 15MB
Có bao giờ anh build Docker image cho một Go app đơn giản mà output ra 1.2GB chưa? Em bị hoài — nhất là mấy hồi mới chơi Docker, cứ docker build xong thấy image to đùng, deploy lên VPS chậm rì. Lý do? Docker image mặc định ship cả SDK, trình biên dịch, package manager — thứ mà runtime không bao giờ cần.
Vấn đề: Fat Image
# Đây là cái anh em hay viết — FATAL
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ày chứa:
- Go compiler + toolchain (~500MB)
- apt packages (gcc, git, ca-certificates...)
- Source code
- Thư mục
pkg/modvới đủ thứ dependency
Vấn đề: SDK không cần thiết ở runtime. Anh đâu có compile code trên production đâu? Vậy tại sao phải mang theo cả đống công cụ đó?
Giải pháp: Multi-Stage Builds
Ý tưởng đơn giản: dùng một image để build, image khác để chạy.
# Stage 1: Builder
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux 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"]
Chỉ có binary + Alpine base — không compiler, không source code, không package manager.
Kết quả đáng kinh ngạc
Em thử nghiệm với một Go REST API crud đơn giản:
| Cách build | Kích thước |
|---|---|
| Single-stage (golang:1.23) | 1.2 GB |
| Multi-stage (golang:alpine → alpine) | 18.5 MB |
Multi-stage + -ldflags="-s -w" + upx |
5.3 MB |
Giảm ~99.5%. Thời gian pull image từ registry cũng chỉ còn vài giây thay vì cả phút.
Chi tiết kỹ thuật
1. -ldflags="-s -w" — Strip debug info
-s: omit symbol table
-w: omit DWARF debug info
Hai flag này nén binary Go xuống ~30-40% vì bỏ debug symbols. Anh vẫn debug được bằng stack trace — chỉ không dùng được delve trên binary này thôi. Nếu deploy production thì không sao.
2. CGO_ENABLED=0
Tắt CGO để binary static link — nghĩa là không phụ thuộc vào libc của hệ thống. Binary chạy được trên bất kỳ Linux distro nào (Alpine, Debian, Distroless...).
3. Distroless — Cực nhẹ và an toàn
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
USER nonroot
CMD ["/server"]
Image base chỉ có binary + thư viện C tối thiểu. Không shell, không package manager, không apt. Nhỏ hơn Alpine (~2MB so với ~5MB).
An toàn hơn vì bề mặt tấn công bằng 0 — hacker không thể apt install curl trong container vì không có apt.
Cache Dependency — Build nhanh hơn
Multi-stage cũng tận dụng Docker layer caching ngon hơn:
FROM golang:1.23-alpine AS builder
WORKDIR /src
# Layer này chỉ thay đổi khi go.mod/sum đổi
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /app/server .
go.mod + go.sum: cache quý hiếm khi đổi → dùng cache- Source code: đổi liên tục → build lại nhanh vì chỉ chạy
go build, không cần download lại dependencies
Kinh nghiệm deploy thực tế
Em đang dùng workflow này cho service Avatar suggestion trên VPS Oracle ARM:
# Build stage
FROM golang:1.23-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOARCH=arm64 go build -ldflags="-s -w" -o bin/service .
# Runtime stage
FROM alpine:3.20
COPY --from=builder /app/bin/service /usr/local/bin/service
# Health check cũng nhẹ
HEALTHCHECK --interval=30s --timeout=3s \
CMD service status || exit 1
EXPOSE 8080
CMD ["service"]
Chú ý GOARCH=arm64 vì VPS là ARM — nếu build trên máy AMD64 (như máy anh Phong) mà quên set thì binary chạy không được.
Checklist tối ưu Docker image
- ✅ Multi-stage build — luôn luôn
- ✅
-ldflags="-s -w"— strip debug - ✅
CGO_ENABLED=0— static binary - ✅ Base image nhỏ —
alpinehoặcdistroless - ✅
.dockerignore— loại.git,node_modules,*.md - ❌ Không dùng
golang:latestở runtime - ✅ Cache dependencies — copy
go.modtrước khi copy source
Kết luận
Multi-stage build là một trong những kỹ thuật DevOps đơn giản mà impact nhất anh có thể áp dụng ngay hôm nay. Chỉ thêm vài dòng Dockerfile, image từ 1.2GB xuống còn ~15MB — deploy nhanh hơn, bảo mật hơn, tốn ít disk hơn.
Còn nếu deploy Go app mà chưa dùng multi-stage... thì mai mốt mỗi lần pull image nhớ tới em nha 😄