Giảm kích thước image container với Docker Multi-Stage Build
(labs.iximiuz.com)- Khi build image container Docker, nếu Dockerfile không có cấu trúc Multi-Stage thì rất dễ bao gồm các tệp không cần thiết
- Điều này dẫn đến kích thước image tăng lên và gia tăng lỗ hổng bảo mật
- Phân tích các nguyên nhân chính tạo ra “các tệp không cần thiết” trong image container và giải thích cách giải quyết bằng Multi-Stage Build
Nguyên nhân khiến kích thước image tăng lên
- Ứng dụng có các phụ thuộc ở thời điểm build và thời điểm chạy.
- Phụ thuộc ở thời điểm build nhiều hơn so với runtime và cũng có nhiều lỗ hổng bảo mật (CVEs) hơn.
- Nếu dùng cùng một image cho cả build và chạy, các phụ thuộc chỉ cần cho build (trình biên dịch, linter, v.v.) sẽ bị đưa vào.
- Image build và image runtime nên được tách riêng, nhưng điều này thường bị bỏ qua.
Ví dụ về cấu trúc Dockerfile sai
Ví dụ sai cho ứng dụng Go
FROM golang:1.23
WORKDIR /app
COPY . .
RUN go build -o binary
CMD ["/app/binary"]
- Image
golang:1.23dùng cho biên dịch, nhưng nếu dùng nguyên image này trong môi trường production thì sẽ bao gồm cả toàn bộ Go compiler và các phụ thuộc của nó. - Kích thước image: hơn 800MB, tồn tại hơn 800 lỗ hổng bảo mật.
Ví dụ sai cho ứng dụng Node.js
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "/app/.output/index.mjs"]
- Thư mục
node_modulessẽ bao gồm cả các phụ thuộc phát triển không cần thiết cho runtime. - Không thể đơn giản sửa bằng
npm ci --omit=dev, vì quá trình build có thể cần đến các phụ thuộc phát triển đó.
Cách tạo image gọn nhẹ trước khi có Multi-Stage Build
Builder pattern
- Build ứng dụng trong
Dockerfile.build:
FROM node:lts-slim
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
- Sao chép artifact đã build ra host:
docker cp $(docker create build:v1):/app/.output .
- Tạo image runtime trong
Dockerfile.run:
FROM node:lts-slim
WORKDIR /app
COPY .output .
CMD ["node", "/app/.output/index.mjs"]
• Vấn đề: phải viết nhiều Dockerfile, cần quản lý thứ tự build và cần thêm script bổ sung.
Tìm hiểu Multi-Stage Build
- Multi-Stage Build là tính năng triển khai Builder pattern ngay bên trong Docker.
- Có thể dùng nhiều lệnh
FROMđể định nghĩa stage build và stage runtime trong một Dockerfile duy nhất. - Dùng lệnh
COPY --from=<stage>để lấy các tệp đã build từ stage trước đó.
- Có thể dùng nhiều lệnh
Ví dụ Dockerfile Multi-Stage (Node.js)
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM node:lts-slim AS runtime
WORKDIR /app
COPY --from=build /app/.output .
ENV NODE_ENV=production
CMD ["node", "/app/.output/index.mjs"]
- Bằng cách sao chép trực tiếp artifact đã build với
COPY --from=build, có thể di chuyển tệp mà không cần đi qua host.
Ví dụ thực tế về Multi-Stage Build
Ứng dụng React
# Build stage
FROM node:lts-slim AS build
WORKDIR /app
COPY . .
RUN npm ci
RUN npm run build
# Runtime stage
FROM nginx:alpine
COPY --from=build /app/build /usr/share/nginx/html
ENTRYPOINT ["nginx", "-g", "daemon off;"]
- Ứng dụng React sau khi build sẽ trở thành các tệp tĩnh và có thể được phục vụ bằng Nginx.
Ứng dụng Go
# Build stage
FROM golang:1.23 AS build
WORKDIR /app
COPY . .
RUN go build -o binary
# Runtime stage
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app/binary /app/binary
ENTRYPOINT ["/app/binary"]
- Dùng image distroless để cung cấp môi trường runtime được tinh gọn tối đa.
Ứng dụng Java
# Build stage
FROM eclipse-temurin:21-jdk-jammy AS build
WORKDIR /build
COPY . .
RUN ./mvnw package -DskipTests
# Runtime stage
FROM eclipse-temurin:21-jre-jammy
COPY --from=build /build/target/app.jar /app.jar
CMD ["java", "-jar", "/app.jar"]
- Khi build dùng JDK, còn runtime dùng JRE nhẹ hơn.
Kết luận
- Multi-Stage Build tách biệt môi trường build và runtime để ngăn kích thước image tăng lên do các phụ thuộc phát triển không cần thiết
- Nhờ đó có thể giảm kích thước image, tăng cường bảo mật và đơn giản hóa quy trình build
- Multi-Stage Build là phương pháp tiêu chuẩn để tạo image container hiệu quả, đồng thời cũng hỗ trợ các tính năng nâng cao (ví dụ: điều kiện phân nhánh, unit test trong khi build)
6 bình luận
Với Java,
jlinkđã được đưa vào từ phiên bản 9, nhưng khả năng sử dụng chưa tốt vì phải tìm rồi chỉ định rõ các mô-đun phụ thuộc bằngjdeps, v.v. Nhìn việc mọi người không biết những cách như vậy hoặc vẫn đi tìm JRE, có vẻ như việc quảng bá các công cụ Java còn thiếu, và cũng cần được cải thiện để chỉ với một lệnh là có thể tạo ra JRE.Tôi cũng đang dùng theo cách đó, nhưng nhược điểm có vẻ là thời gian build khá lâu.
Thời gian build lẽ ra không nên khác nhau. Nếu có khác biệt thì là do cấu hình sai!
À, ra là vậy!
Tùy theo chiến lược, có thể cache nguyên cả một stage, nên ngược lại với mình thời gian build còn được rút ngắn nữa!
Chắc tôi cần tìm hiểu thêm về Docker rồi!