Tối Ưu Hóa Docker Cho Ứng Dụng NestJS (Multi-Stage Build)

,

・Published on:

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ơnimage 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ới RUN yarn install. Điều này đảm bảo Docker chỉ chạy lại lệnh yarn install khi 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ừ stage base để chạy npm 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_modules chỉ dành cho production.
  • Điểm mạnh: Đây là bí quyết giảm dung lượng. Lệnh npm prune --production sẽ 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_modules từ 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 --production là 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 đó.
# 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ỉ COPY những thứ cần thiết:
      1. node_modules đã được prune (từ bundle_node_modules).
      2. 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.