Multi-stage build: cách thu nhỏ Docker image hiệu quả

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Image Docker nặng vài trăm MB không chỉ tốn băng thông mà còn chứa nhiều công cụ thừa, tăng rủi ro bảo mật. Multi-stage build cho phép bạn dùng công cụ nặng để build, nhưng chỉ giữ lại thành phẩm khi chạy thật. Kết quả thường nhỏ gọn hơn rất nhiều.

Vấn đề với image một giai đoạn

Khi build ứng dụng, bạn cần trình biên dịch, bộ cài thư viện, công cụ test. Nhưng khi chạy, bạn chỉ cần file đã build và runtime. Nếu gom hết vào một image, mọi thứ build đều bị mang theo lên môi trường production.

Ý tưởng cốt lõi

Trong một Dockerfile bạn có thể có nhiều lệnh FROM. Mỗi lệnh mở một giai đoạn (stage) mới. Bạn có thể chép file từ giai đoạn trước sang giai đoạn sau bằng COPY --from. Image cuối cùng chỉ gồm giai đoạn cuối, mọi thứ ở giai đoạn trước bị bỏ đi.

Ví dụ với ứng dụng Go

Go biên dịch ra một file chạy độc lập nên là ví dụ rõ nhất:

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app .

FROM alpine:3.20
RUN adduser -D appuser
USER appuser
COPY --from=build /out/app /usr/local/bin/app
EXPOSE 8080
CMD app

Giai đoạn build dùng image Go đầy đủ, nặng cả trăm MB. Giai đoạn cuối chỉ là Alpine nhỏ xíu cộng một file thực thi, nên image cuối thường chỉ vài chục MB.

Ví dụ với ứng dụng web dùng Node.js

Với giao diện React hay Vue, bạn build ra file tĩnh rồi phục vụ bằng Nginx:

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:1.27-alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80

Toàn bộ node_modules và mã nguồn gốc nằm lại ở giai đoạn build. Image cuối chỉ chứa Nginx và các file tĩnh. Lưu ý thư mục kết quả build có thể là dist hoặc build tùy công cụ bạn dùng.

Lợi ích thực tế

  • Nhỏ hơn: tải nhanh, khởi động nhanh, tiết kiệm dung lượng registry.
  • An toàn hơn: không còn trình biên dịch hay công cụ thừa cho kẻ xấu lợi dụng.
  • Gọn gàng: một Dockerfile duy nhất thay vì hai file và một script nối.
  • Dễ kiểm thử: có thể thêm giai đoạn test và build dừng lại tại đó bằng --target.

Một số mẹo bổ sung

  1. Đặt tên giai đoạn bằng AS để tham chiếu dễ đọc hơn số thứ tự.
  2. Sắp xếp lệnh từ ít thay đổi đến hay thay đổi để tận dụng cache.
  3. Chọn image nền cuối nhỏ như alpine hoặc bản slim, nhưng nhớ rằng alpine dùng thư viện musl nên một số gói native có thể cần kiểm tra thêm.
  4. Chạy bằng người dùng không phải root ở giai đoạn cuối.
  5. Dùng lệnh docker images trước và sau khi tối ưu để thấy hiệu quả thật.

Khi nào chưa cần?

Nếu bạn chỉ chạy thử trong máy mình hoặc dự án học tập, một giai đoạn là đủ. Nhưng khi đưa lên môi trường thật, multi-stage gần như nên là mặc định vì chi phí thêm vào rất ít mà lợi ích rõ ràng.

Kết luận

Multi-stage build tách việc 'xây nhà' khỏi việc 'ở trong nhà': giàn giáo bỏ lại, chỉ giữ công trình hoàn thiện. Hãy thử viết lại Dockerfile hiện có của bạn theo cách này và so sánh dung lượng.

Để nắm vững cả chuỗi từ đóng gói, tối ưu đến triển khai, bạn có thể học dần theo lộ trình tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment