Dockerize ứng dụng Go: Tuyệt chiêu ép size image từ 800MB xuống còn 10MB

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

Container “béo phì” và nỗi ám ảnh tốc độ deploy

Hồi mới tập tành Dockerize ứng dụng Go, mình cứ nghĩ mọi thứ chỉ đơn giản là đóng gói rồi chạy. Mình viết một cái Dockerfile “sách giáo khoa”: dùng FROM golang:latest, copy code, chạy go build. Kết quả làm mình đứng hình. Một ứng dụng Hello World đơn giản mà image nặng tới gần 800MB.

Thử tưởng tượng cảnh deploy lên một con VPS rẻ tiền hay chạy qua mạng 4G chập chờn. Ngồi đợi pull cái image nặng trịch đó thực sự là cực hình. Nó không chỉ tốn dung lượng lưu trữ mà còn làm quy trình CI/CD chậm chạp một cách vô lý. Go vốn nổi tiếng với khả năng tạo binary siêu gọn, vậy 800MB kia từ đâu ra?

Mổ xẻ nguyên nhân: Tại sao Image lại nặng đến thế?

Vấn đề không nằm ở code của bạn. Nó nằm ở cái “gốc” (base image) bạn chọn. Khi dùng golang:latest dựa trên Debian hoặc Ubuntu, bạn đang vác theo cả một hệ điều hành đầy đủ vào container.

  • Bộ công cụ build cồng kềnh: Image chứa sẵn compiler, debugger và hàng tá thư viện C nặng nề. Thực tế, khi chạy (runtime), ứng dụng của bạn hoàn toàn không cần đến chúng.
  • Package thừa thãi: Các tiện ích hệ thống, shell, trình quản lý package… chiếm hàng trăm MB nhưng lại nằm đắp chiếu.
  • Lỗ hổng bảo mật: Càng nhiều phần mềm cài sẵn, bề mặt tấn công càng rộng. Hacker rất thích những container có sẵn curl hay apt để thực hiện leo thang đặc quyền.

Chiến thuật “giảm cân” từ nghiệp dư đến chuyên nghiệp

Sau nhiều lần thử sai trên các dự án production, mình rút ra 3 cấp độ tối ưu rõ rệt.

Cấp độ 1: Alpine Linux – Giải pháp “mì ăn liền”

Thay vì dùng image mặc định, hãy chuyển sang Alpine. Đây là bản phân phối siêu nhẹ, chỉ nặng khoảng 5MB.

FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]

Chỉ với thay đổi này, image giảm xuống còn khoảng 300MB. Khá hơn nhiều, nhưng vẫn chưa phải là tối ưu nhất vì nó vẫn chứa cả bộ Go SDK bên trong.

Cấp độ 2: Multi-stage Build – Cuộc cách mạng về tư duy

Đây là kỹ thuật mình tâm đắc nhất và là tiêu chuẩn cho mọi dự án hiện đại. Ý tưởng rất đơn giản: Chia quá trình đóng gói thành hai giai đoạn độc lập.

  • Giai đoạn 1 (Builder): Dùng image đầy đủ để biên dịch code thành file binary.
  • Giai đoạn 2 (Runner): Chỉ copy duy nhất file binary đó sang một image cực nhẹ để chạy.

Toàn bộ mã nguồn, cache và bộ compiler sẽ bị loại bỏ hoàn toàn khỏi image cuối cùng.

Scratch Image – “Trùm cuối” của tối ưu kích thước

Nếu Alpine vẫn chưa làm bạn hài lòng, hãy dùng scratch. Đây là một image rỗng hoàn toàn (0 bytes). Vì Go có thể build ra các static binary tự thân vận động, nó chẳng cần hệ điều hành nào bao quanh để chạy cả.

Dockerfile mẫu chuẩn Production mình đang áp dụng

Dưới đây là cấu hình mình đã tinh chỉnh, vừa đảm bảo an toàn, vừa nhẹ đến mức kinh ngạc.

# Giai đoạn 1: Build binary
FROM golang:1.22-alpine AS builder

# Cài đặt các thư viện bổ trợ cần thiết
RUN apk update && apk add --no-cache git ca-certificates tzdata

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .

# Build với các flag tối ưu dung lượng
# CGO_ENABLED=0: Tạo static binary hoàn toàn
# -ldflags="-w -s": Xóa thông tin debug, giảm thêm ~20% kích thước
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./main.go

# Giai đoạn 2: Runner siêu gọn nhẹ
FROM scratch

# Copy chứng chỉ bảo mật và múi giờ
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

Kết quả? Image cuối cùng chỉ nặng vỏn vẹn 12MB. Thời gian pull image giờ chỉ tính bằng giây thay vì bằng phút.

Những “cú lừa” cần tránh khi dùng Scratch

Dùng scratch sướng nhưng rất dễ dính bẫy nếu bạn chưa có kinh nghiệm xử lý lỗi runtime.

1. Lỗi kết nối HTTPS

scratch rỗng tuếch, nó không có sẵn CA Certificates. Nếu app của bạn cần gọi API bên thứ ba (như Stripe hay AWS), nó sẽ báo lỗi SSL ngay lập tức. Đừng quên copy file ca-certificates.crt từ stage builder như ví dụ trên.

2. Debug trong bóng tối

Trong scratch không có ls, không có bash, cũng chẳng có curl. Bạn không thể docker exec vào để kiểm tra file.

Mẹo của mình là hãy log mọi thứ ra stdout theo định dạng JSON. Khi cần soi lỗi từ log hệ thống, mình hay dùng JSON Formatter tại Toolcraft để format lại cho dễ đọc. Việc này nhanh và sạch sẽ hơn nhiều so với việc cố cài thêm công cụ debug vào container production.

Thành quả sau 6 tháng thực chiến

Tối ưu image không phải để cho đẹp con số. Sau khi áp dụng cho hệ thống microservices của công ty, mình nhận thấy 3 lợi ích sát sườn:

  • Deploy thần tốc: Thời gian từ lúc commit code đến khi app chạy trên server giảm từ 5 phút xuống còn dưới 1 phút.
  • Tiết kiệm hầu bao: Dung lượng lưu trữ trên AWS ECR giảm đáng kể, giúp tiết kiệm chi phí lưu trữ hàng tháng.
  • An tâm tuyệt đối: Image không có shell đồng nghĩa với việc hacker mất đi công cụ cơ bản nhất để phá hoại nếu chẳng may xâm nhập được vào container.

Nếu bạn đang làm Go, hãy thử ép size image ngay hôm nay. Có thể ban đầu hơi lạ lẫm với việc xử lý certificate, nhưng tin mình đi, kết quả sẽ cực kỳ xứng đáng.

Share: