Hello anh em, Dũng Lê quay lại rồi đây, anh em làm devops hay làm dev thì gần như 100% là phải đối diện với Docker rồi. Docker đã trở thành công cụ không thể thiếu trong việc phát triển và triển khai ứng dụng. Tuy nhiên, hôm qua tôi review thì thấy 1 bạn trong team build Docker image có size quá lớn, ảnh hưởng đến tốc độ tải, thời gian triển khai và chi phí lưu trữ(~1,7GB). Bài này chúng ta cùng nhào nặn để giảm thiểu độ nặng của Docker image nhé
Khi mới bắt đầu
Đây là Dockerfile rất cơ sở khi chúng ta sử dụng, ở đây tôi đang build nestjs app
FROM node:20
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn
COPY . .
RUN yarn build
EXPOSE 3000
CMD ["node", "dist/main.js"]
Đây là một Dockerfile đơn giản, chẳng có gì đặc biệt cả. Bạn chỉ việc sử dụng node:20
là image mặc định, sao chép các file cần thiết, cài đặt dependencies, build ứng dụng và chạy nó. Quá trình này nghe có vẻ rất hợp lý, đúng không?
Tuy nhiên, vấn đề lớn là… kích thước image sau khi build xong có thể lên đến 1,7GB! Tôi chạy build Dockerfile này với tag là 1.0.0 kích thước image sẽ rất lớn như trong hình các bạn có thể thấy

Vậy làm sao để giải quyết vấn đề này?
Thay đổi đầu tiên
Sau một thời gian tìm hiểu, tôi đã tìm thấy một giải pháp đơn giản nhưng hiệu quả: sử dụng Alpine. Alpine là một hệ điều hành cực kỳ nhẹ (chỉ khoảng 5MB) được thiết kế đặc biệt để sử dụng trong môi trường container. Thay vì sử dụng image node:20
, tôi chuyển sang node:20-alpine
.
Cập nhật Dockerfile của tôi như sau:
# User alpine image
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]
Bằng cách này, Docker image của tôi đã nhẹ hơn rất nhiều so với việc sử dụng image mặc định. Tôi chạy build Dockerfile này với tag là 1.0.1 kích thước đã giảm xuống chỉ còn ~750Mb – một thay đổi thực sự rất đáng để kể

Tuy nhiên, Dũng Lê là kẻ tham lam, và nhu cầu lớn, kết quả như thế này thì vẫn chưa thoả mãn được bản thân tôi, hãy xem tôi làm gì tiếp theo nhé!
Áp dụng thêm kỹ thuật mới
Lục lọi lại kiến thức đã có, nạp thêm 1 mớ kiến thức từ AI thì tôi tìm thấy 1 kỹ thuật thường được sử dụng để tối ưu hoá Docker multi-stage build. Kỹ thuật này chia quá trình xây dựng image thành nhiều giai đoạn khác nhau, mỗi giai đoạn có một nhiệm vụ riêng biệt, và cuối cùng chỉ mang theo những gì thật sự cần thiết.
# Build stage - User alpine image
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./ yarn.lock ./
RUN yarn
COPY . .
RUN yarn build
# Production stage
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]
Dockerfile mới sử dụng node:20-alpine
để cài đặt phụ thuộc và build ứng dụng. Sau đó, vào giai đoạn thứ hai, Dockerfile chỉ sao chép các file cần thiết từ giai đoạn build như node_modules
và thư mục dist
, bỏ qua tất cả các file không cần thiết khác, vì nestjs sử dụng typescript sau khi build thì chúng ta sử chạy ứng dụng với thư mục dist nghĩa là thư mục chứa code đã được build. Bằng cách này, Dockerfile tạo ra một image cực kỳ nhẹ chỉ chứa những gì thật sự cần thiết để chạy ứng dụng. Tôi chạy build Dockerfile này với tag là 1.0.2 kích thước đã giảm xuống chỉ còn 336Mb

So với độ nặng của image ban đầu là 1.7GB thì hiện tại đã quá thành công rồi, đổi với 1 image có độ nặng là 336MB chúng ta hoàn toàn có thể chấp nhận được. Tuy nhiên sự tham lam của tôi vẫn chưa hết, theo dõi tiếp xem tôi sẽ bào docker tới đâu nhé!
Tuyệt chiêu cuối – Tối ưu từng chi tiết
Tiếp tục nghĩ ngợi tôi phát hiện rằng, mình tạo thêm 1 stage sau khi build xong thì chỉ cần những package trong production dependencies là được, lục lọi thêm thì kỹ thuật này có tên pruner stage. Đây là một kỹ thuật tối ưu hóa thêm nữa bằng cách sử dụng một giai đoạn “pruner”, giúp loại bỏ các phụ thuộc không cần thiết và dọn dẹp cache để giảm thiểu kích thước image.
# Stage 1: Build stage - User alpine image
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
# Stage 2: Pruner stage - Just to keep the production dependencies
FROM node:20-alpine AS pruner
WORKDIR /app
COPY package*.json yarn.lock ./
RUN yarn install --production && yarn cache clean
# Stage 3: Runtime stage
FROM node:20-alpine
WORKDIR /app
COPY --from=pruner /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]
Tới bước này, Dockerfile chứa ba giai đoạn: một giai đoạn build, một giai đoạn pruner để chỉ giữ lại các phụ thuộc production và xóa cache, và cuối cùng là giai đoạn runtime. Cách tiếp cận này giúp image cuối cùng trở nên cực kỳ nhẹ nhàng, chỉ chứa các phụ thuộc cần thiết và loại bỏ tất cả các thứ thừa thãi. Tôi chạy build Dockerfile này với tag là 1.0.3 kích thước đã giảm xuống chỉ còn 145Mb

Kết Thúc Câu Chuyện
Và thế là hành trình tối ưu hóa Docker image đã kết thúc. Từ một Dockerfile đơn giản, qua từng giai đoạn tối ưu hóa, chúng ta đã học được cách giảm kích thước image một cách hiệu quả bằng cách sử dụng các kỹ thuật như chuyển sang Alpine, sử dụng multi-stage build và thêm pruner stage để dọn dẹp mọi thứ không cần thiết.
Cuối cùng, Dockerfile của chúng ta giờ đây trở nên tối ưu, nhẹ nhàng và hiệu quả hơn bao giờ hết. Và mỗi lần xây dựng image mới, chúng ta đều biết rằng nó sẽ chiếm ít không gian hơn, tải nhanh hơn, và hoạt động mượt mà hơn.
Lưu ý nhẹ
Với 1 số ứng dụng dùng thư viện phức tạp, tương tác với hệ điều hành sâu có thể alpine image sẽ không có sẵn các services, libs,… nên các bạn có thể thêm bước cài services, libs, hoặc lựa chọn image gốc khác như slim, Minimal, Distroless
Nếu bạn thấy blog này hữu ích, hãy chia sẻ với cộng đồng Linux Việt và nhớ ủng hộ blog của mình nhé.
Việt Nam Linux Family – Cộng đồng Linux của người Việt. ✌️🐧🇻🇳
Leave a Reply