Docker Multi-Stage Builds — Từ 1.2GB xuống 15MB

Phong Hy

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/mod vớ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

  1. ✅ Multi-stage build — luôn luôn
  2. -ldflags="-s -w" — strip debug
  3. CGO_ENABLED=0 — static binary
  4. ✅ Base image nhỏ — alpine hoặc distroless
  5. .dockerignore — loại .git, node_modules, *.md
  6. ❌ Không dùng golang:latest ở runtime
  7. ✅ Cache dependencies — copy go.mod trướ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 😄