NestJS là framework Node.js tuyệt vời, nhưng việc Dockerize không đúng cách có thể dẫn đến dung lượng image khổng lồ (hàng GB) và thời gian build chậm. Image cuối cùng bị nhiễm các tệp không cần thiết (devDependencies, source code gốc, công cụ build). Giải pháp: Kỹ thuật Multi-Stage Build trong Dockerfile, cam kết mang lại tốc độ build nhanh hơn và image nhỏ gọn, bảo mật hơn.
Xem ngay file Dockerfile dưới đây rồi cùng đi vào bàn luận
# Layer base install node_modules
FROM node:23-alpine3.20 AS base
WORKDIR /app
COPY package.json yarn.lock .yarnrc.yml .
RUN corepack enable
RUN yarn install --immutable
# Layer get production node_modules
FROM node:23-alpine3.20 AS bundle_node_modules
WORKDIR /app
COPY package.json yarn.lock .
COPY --from=base /app/node_modules node_modules
RUN npm prune --production
# Layer builder build app from source
FROM node:23-alpine3.20 AS builder
WORKDIR /app
COPY package.json yarn.lock tsconfig.build.json tsconfig.json ./
COPY --from=base /app/node_modules node_modules
COPY src src
RUN npm run build
# Final image
FROM node:23-alpine3.20 AS runner
WORKDIR /app
COPY --from=bundle_node_modules /app/node_modules node_modules
COPY --from=bundle_node_modules /app/package.json .
COPY --from=builder /app/dist dist
COPY .env .env
EXPOSE 3000
ENV NODE_ENV=production
CMD [ "npm", "run", "start:prod" ]
Stage 1: base – Tối ưu Caching Dependencies
- Mục tiêu: Cài đặt toàn bộ dependencies (bao gồm
devDependencies) chỉ một lần. - Điểm mạnh:
- Sử dụng Alpine để có base image nhẹ.
- Kỹ thuật Cache Vàng: Chỉ
COPY package.json… trước, sau đó mớiRUN yarn install. Điều này đảm bảo Docker chỉ chạy lại lệnhyarn installkhi file cấu hình thay đổi. - Layer này mục tiêu là chỉ chậm lần đầu và những lần file package.js thay đổi. Thông thường package.json rất ít khi thay đổi, nên việc cache lại lớp này sớm là điều cần thiết.
FROM node:23-alpine3.20 AS base
WORKDIR /app
COPY package.json yarn.lock .yarnrc.yml .
RUN corepack enable
RUN yarn install --immutable
Stage 2: builder – Phân Tách Công Việc Build
- Mục tiêu: Biên dịch mã nguồn (TS sang JS).
- Điểm mạnh: Sử dụng
node_modulesđầy đủ từ stagebaseđể chạynpm run build. Sau khi build, các tệp tạm thời này sẽ bị loại bỏ hoàn toàn.- Bằng cách chỉ copy những file cần thiết, kỹ thuật này giúp ta tránh phải build lại toàn bộ từ đầu.
- Các bài viết khác trên mạng thuơng hướng dẫn dùng lệnh
COPY . .điều này ko tối ưu, khi build lại image mặc dù source ko thay đổi nhưng cache ko được sử dụng, dẫn đến mất thời gian build, tốn CPU, RAM.
# Layer builder build app from source
FROM node:23-alpine3.20 AS builder
WORKDIR /app
COPY package.json yarn.lock tsconfig.build.json tsconfig.json ./
COPY --from=base /app/node_modules node_modules
COPY src src
RUN npm run build
Stage 3: bundle_node_modules – “Cắt Tỉa” cho Production
- Mục tiêu: Chuẩn bị
node_moduleschỉ dành cho production. - Điểm mạnh: Đây là bí quyết giảm dung lượng. Lệnh
npm prune --productionsẽ dọn dẹp và xóa tất cả các gói chỉ dùng để phát triển.- Lợi ích: Giảm kích thước
node_modulestừ vài trăm MB xuống chỉ còn vài chục MB. - Khi build môi trường product ta thường phải remove đi các devDependencies để tránh image quá nặng, chứa source ko cần thiết. Ta tách riêng 1 stage chỉ để làm nhiệm vụ này, khi stage 1 không phải build lại thì stage này cũng được cache lại toàn bộ luôn.
- Lệnh
npm prune --productionlà lệnh cực kì tiêu tốn nhiều RAM, CPU. Bước này mà được cache là bạn tiết kiệm được nhiều tài nguyên máy lắm đó.
- Lợi ích: Giảm kích thước
# Layer get production node_modules
FROM node:23-alpine3.20 AS bundle_node_modules
WORKDIR /app
COPY package.json yarn.lock .
COPY --from=base /app/node_modules node_modules
RUN npm prune --production
Stage Cuối Cùng: runner – Image Production “Sạch”
- Mục tiêu: Tạo ra image nhỏ nhất và bảo mật nhất để chạy ứng dụng.
- Điểm mạnh:
- Bắt đầu lại với image Alpine sạch sẽ.
- Chỉ
COPYnhững thứ cần thiết:node_modulesđã được prune (từbundle_node_modules).- Mã nguồn đã biên dịch (
dist/) (từbuilder).
- Kết luận: Image cuối cùng không chứa source code TS, công cụ build, hoặc các file không cần thiết khác.
FROM node:23-alpine3.20 AS runner
WORKDIR /app
COPY --from=bundle_node_modules /app/node_modules node_modules
COPY --from=bundle_node_modules /app/package.json .
COPY --from=builder /app/dist dist
COPY .env .env
Kết luận
Kỹ thuật Multi-Stage Build không chỉ là một thủ thuật; đó là tiêu chuẩn vàng để đóng gói các ứng dụng Node.js, đặc biệt là NestJS, trong môi trường Production. Bằng cách tách biệt rõ ràng giữa giai đoạn cài đặt dependencies (base), build (builder), cắt tỉa (bundle_node_modules) và chạy (runner), chúng ta đã giải quyết triệt để hai vấn đề lớn nhất: giảm thời gian build nhờ caching hiệu quả và giảm dung lượng image xuống mức tối thiểu.
Giờ đây, bạn có thể tự tin triển khai các dịch vụ NestJS nhỏ, nhanh và bảo mật hơn bao giờ hết. Hãy áp dụng Dockerfile này ngay hôm nay để đưa quy trình DevOps của bạn lên một tầm cao mới.