
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:20dự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 installsẽ 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ệuREADME.md, hoặc thậm chí là thư mụcnode_modulescó 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 trongpackage-lock.jsonvà chạy nhanh hơn.npm prune --production: Loại bỏ cácdevDependenciessau khi build xong để làm sạch thư mụcnode_modules.COPY --chown=node:node: Phân quyền sở hữu file cho usernodengay 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 Image | 1.22 GB | 94.5 MB | Giảm ~92% |
| Thời gian Build (CI/CD) | 3 phút 15 giây | 45 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) | 0 | An 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:
- Luôn sử dụng
.dockerignore. - Ưu tiên các Base Image dạng
-alpinehoặc-slim. - 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.
