Tối Ưu Hóa Docker Cho Ứng Dụng Node.js

5 min read

Trong kỷ nguyên của Microservices và Cloud Native, Docker đã trở thành một công cụ không thể thiếu đối với mọi lập trình viên tại NCC. Tuy nhiên, có một thực tế là rất nhiều dự án Node.js hiện nay đang “đóng gói” Docker Image một cách khá sơ sài. Việc lạm dụng cấu hình mặc định khiến một ứng dụng Web đơn giản có thể nặng tới hơn 1GB.

Một Docker Image quá nặng sẽ kéo theo nhiều hệ lụy: tốn dung lượng lưu trữ (Registry), kéo dài thời gian deploy (CI/CD pipeline) và tiềm ẩn nhiều lỗ hổng bảo mật. Để cập nhật thêm các kiến thức nền tảng khác, bạn có thể tham khảo thêm các bài viết chất lượng tại chuyên mục DevOps của ANT NCC.


1. Vấn Đề Với Cách Viết Dockerfile Thông Thường

Hãy nhìn vào một Dockerfile “quốc dân” mà chúng ta thường bắt gặp dưới đây:

Dockerfile

# Dockerfile chưa tối ưu
FROM node:20

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["node", "server.js"]

Khi chạy lệnh docker build, Image tạo ra sẽ có dung lượng cực kỳ lớn (thường khoảng 1.1GB đến 1.3GB). Lý do là vì:

  • Base Image quá lớn: node:20 dựa trên bản phân phối Debian đầy đủ, chứa nhiều công cụ build, thư viện hệ thống không bao giờ dùng tới trong môi trường Production.
  • Chứa cả devDependencies: Lệnh npm install sẽ cài đặt toàn bộ các thư viện test, linter (như Jest, ESLint).
  • Rác từ mã nguồn: Copy toàn bộ file bao gồm cả .git, tài liệu README.md, hoặc thậm chí là thư mục node_modules có sẵn ở máy local nếu quên cấu hình.

2. Các Kỹ Thuật Tối Ưu Hóa Docker Image “Xương Máu”

Để giải quyết bài toán trên, chúng ta cần áp dụng đồng thời 4 kỹ thuật cốt lõi sau:

2.1. Sử dụng .dockerignore

Tương tự như .gitignore, file .dockerignore giúp loại bỏ các file không cần thiết trước khi Docker thực hiện lệnh COPY . ..

Tạo file .dockerignore ở thư mục gốc của dự án:

Plaintext

node_modules
npm-debug.log
Dockerfile
.git
.github
README.md

2.2. Lựa chọn Base Image phù hợp (Alpine hoặc Slim)

Thay vì dùng node:latest hoặc node:20, hãy chuyển sang các phiên bản rút gọn:

  • node:20-slim: Chỉ chứa các package tối thiểu cần thiết để chạy Node.js.
  • node:20-alpine: Dựa trên Alpine Linux, một bản phân phối siêu nhẹ (chỉ khoảng 5MB). Bản Image Node-alpine hoàn chỉnh thường chỉ nặng khoảng 40MB.

2.3. Áp dụng Multi-stage Builds

Đây là kỹ thuật quan trọng nhất. Cấu trúc Multi-stage cho phép chúng ta chia quá trình build thành nhiều giai đoạn (Stages). Chúng ta có thể dùng một Image đầy đủ công cụ để build code (TypeScript, cài devDependencies), sau đó chỉ copy sản phẩm cuối cùng (file JS đã build và production node_modules) sang một Image siêu nhẹ để chạy.

2.4. Không chạy ứng dụng bằng quyền Root (Bảo mật)

Mặc định, Docker chạy các tiến trình với quyền root. Nếu ứng dụng Node.js của bạn bị hacker khai thác qua lỗ hổng code, họ sẽ chiếm luôn quyền điều khiển container. Image Node.js chính thức đã tạo sẵn một user tên là node, chúng ta nên tận dụng user này.


3. Thực Hành: Dockerfile Chuẩn Production Sau Khi Tối Ưu

Dưới đây là Dockerfile áp dụng toàn bộ các Best Practices trên cho một dự án Node.js (hỗ trợ cả TypeScript):

Dockerfile

# --- STAGE 1: Build Stage ---
FROM node:20-alpine AS builder

WORKDIR /app

# Copy các file cấu hình package
COPY package*.json ./

# Cài đặt tất cả dependencies (bao gồm cả devDependencies để build TS)
RUN npm ci

# Copy toàn bộ mã nguồn
COPY . .

# Build TypeScript (nếu có, kết quả ra thư mục /dist)
RUN npm run build

# Xóa node_modules cũ và chỉ cài đặt các package cần cho Production
RUN npm prune --production

# --- STAGE 2: Runner Stage ---
FROM node:20-alpine AS runner

WORKDIR /app

# Thiết lập môi trường Production
ENV NODE_ENV=production

# Bảo mật: Sử dụng user phi tập trung 'node' có sẵn trong image alpine
USER node

# Chỉ copy những file thực sự cần thiết từ Stage Build
COPY --chown=node:node package*.json ./
COPY --chown=node:node --from=builder /app/node_modules ./node_modules
COPY --chown=node:node --from=builder /app/dist ./dist

EXPOSE 3000

CMD ["node", "dist/server.js"]

Giải thích các cải tiến trong Dockerfile mới:

  • FROM node:20-alpine AS builder: Đặt tên cho stage đầu tiên là builder.
  • npm ci: Thay vì npm install, lệnh này giúp cài đặt chính xác các version trong package-lock.json và chạy nhanh hơn.
  • npm prune --production: Loại bỏ các devDependencies sau khi build xong để làm sạch thư mục node_modules.
  • COPY --chown=node:node: Phân quyền sở hữu file cho user node ngay khi copy để tránh lỗi permission khi chạy với quyền non-root.

4. Kết Quả Đạt Được (Benchmark)

Dưới đây là bảng so sánh trực quan hiệu quả trước và sau khi tối ưu hóa trên một dự án thực tế tại NCC:

Tiêu chíTrước khi tối ưu (node:20)Sau khi tối ưu (Multi-stage + Alpine)Hiệu quả
Dung lượng Image1.22 GB94.5 MBGiảm ~92%
Thời gian Build (CI/CD)3 phút 15 giây45 giây (tận dụng cache tốt)Nhanh hơn 4 lần
Lỗ hổng bảo mật (Vulnerabilities)84 (2 Critical, 12 High)0An toàn tuyệt đối

Hình ảnh minh họa kết quả quét bảo mật bằng Trivy hoặc Docker Scout:

(Alt text: Bảng so sánh dung lượng Docker Image Nodejs và mức độ bảo mật trước sau tối ưu)


Kết Luận & Takeaway

Tối ưu hóa Docker Image không chỉ là câu chuyện tiết kiệm tài nguyên phần cứng, mà nó ảnh hưởng trực tiếp đến hiệu suất vận hành của hệ thống CI/CD và mức độ an toàn thông tin của dự án.

3 điểm cốt lõi bạn cần nhớ cho dự án tiếp theo:

  1. Luôn sử dụng .dockerignore.
  2. Ưu tiên các Base Image dạng -alpine hoặc -slim.
  3. Tách biệt môi trường Build và môi trường Run bằng Multi-stage build.

Hy vọng bài viết này mang lại giá trị thiết thực cho các bạn Dev cũng như DevOps tại NCC trong các dự án sắp tới.

Avatar photo

Leave a Reply

Your email address will not be published. Required fields are marked *