18 điểm bởi xguru 2024-11-17 | 6 bình luận | Chia sẻ qua WhatsApp
  • 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.23 dù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_modules sẽ 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

  1. Build ứng dụng trong Dockerfile.build:
FROM node:lts-slim  
WORKDIR /app  
COPY . .  
RUN npm ci  
RUN npm run build  
  1. Sao chép artifact đã build ra host:
docker cp $(docker create build:v1):/app/.output .  
  1. 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 đó.

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

 
savvykang 2024-11-18

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ằng jdeps, 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.

 
brainer 2024-11-17

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.

 
kandk 2024-11-18

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!

 
brainer 2024-11-18

À, ra là vậy!

 
qurare 2024-11-18

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!

 
brainer 2024-11-18

Chắc tôi cần tìm hiểu thêm về Docker rồi!