Bí quyết bảo mật khi deploy NodeJS bằng Docker trên VPS cho môi trường thực chiến
Deploy NodeJS lên VPS bằng Docker nghe rất “gọn”: build image, chạy container, mở port, xong. Nhưng production thật không vận hành theo kiểu “xong là xong”. Phần lớn sự cố không đến từ bug logic, mà từ các lỗ hổng triển khai: container chạy root, lộ .env, image phình to, port DB mở public, SSH yếu, log chứa secret, không giới hạn quyền process. Chỉ cần một mắt xích lỏng → toàn bộ VPS thành mục tiêu.
Bài này tập trung vào bảo mật thực chiến: ít lý thuyết, nhiều nguyên tắc áp dụng ngay. Mục tiêu không phải “bất khả xâm phạm”, mà là giảm mạnh bề mặt tấn công, hạn chế thiệt hại khi có sự cố, đồng thời vẫn giữ quy trình deploy đủ đơn giản để team vận hành lâu dài.
1. Tư duy đúng: bảo mật là nhiều lớp
Sai lầm phổ biến: nghĩ rằng “dùng Docker là an toàn hơn”. Docker không tự tạo bảo mật. Nếu cấu hình kém, container chỉ là một lớp mỏng trên VPS.
Nguyên tắc thực chiến:
– VPS phải cứng: SSH an toàn, firewall chặt, cập nhật định kỳ.
– Docker image phải tối giản: ít package, ít binary, ít rủi ro.
– Container runtime phải bị giới hạn quyền.
– App NodeJS phải không lộ secret, không log nhạy cảm, xử lý input đúng.
– Network phải đóng mặc định, chỉ mở cái cần.
– Quan sát phải đủ: log, healthcheck, cảnh báo, backup.
Tư duy đúng: giả định có ngày một lớp bị xuyên thủng. Khi đó, các lớp còn lại phải làm chậm, chặn, hoặc giảm blast radius.
2. Bảo vệ VPS trước khi nói tới Docker
Nhiều người chăm chăm harden container nhưng lại để VPS mở SSH bằng password. Đó là “khóa cửa sau nhưng mở toang cửa trước”.
Các bước tối thiểu nên làm
– Tắt đăng nhập SSH bằng password, chỉ dùng SSH key.
– Đổi port SSH không phải biện pháp chính, nhưng giúp giảm bot scan rác.
– Tắt đăng nhập root qua SSH.
– Bật firewall: chỉ mở 22, 80, 443; nếu có service nội bộ thì bind private network.
– Cập nhật hệ điều hành định kỳ: vá kernel, OpenSSL, Docker, Node runtime.
– Dùng fail2ban hoặc giải pháp tương đương để chặn brute force SSH.
Nếu bạn chạy PostgreSQL/Redis/MySQL trên cùng VPS, không mở port DB ra Internet trừ khi thật sự bắt buộc. App container nên kết nối qua Docker network nội bộ hoặc 127.0.0.1.
3. Chọn image NodeJS tối giản, giảm bề mặt tấn công
Image càng to → càng nhiều package → càng nhiều CVE tiềm năng. Production không cần image “tiện đủ thứ”.
Khuyến nghị
– Ưu tiên base image nhỏ như node:20-alpine hoặc image slim phù hợp nhu cầu.
– Dùng multi-stage build để tách bước build và runtime.
– Chỉ copy artifact cần thiết sang runtime image.
– Không giữ tool build như gcc, python, git trong image cuối nếu không cần.
Ví dụ:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --from=builder /app/dist ./dist
USER node
EXPOSE 3000
CMD ["node", "dist/server.js"]
Điểm quan trọng:
– npm ci → cài dependency nhất quán theo lockfile.
– --omit=dev → loại dev dependency khỏi production.
– USER node → tránh chạy container bằng root.
4. Đừng chạy container bằng root
Đây là lỗi rất phổ biến. Nếu app bị RCE hoặc dependency bị khai thác, attacker có quyền của process trong container. Nếu process chạy root, rủi ro tăng mạnh.
Cách làm đúng
– Dùng user không đặc quyền: USER node hoặc tạo user riêng.
– Chỉ cấp quyền ghi cho thư mục thật sự cần, ví dụ thư mục upload/temp.
– Nếu có thể, mount filesystem ở chế độ read-only.
read_only: true rất hữu ích: app chỉ đọc code/runtime, giảm khả năng ghi file trái phép sau khi bị xâm nhập.
5. Quản lý secret: tuyệt đối không bake vào image
Lỗi nguy hiểm nhất: copy .env vào image hoặc commit secret vào Git. Một khi secret đã vào image registry hoặc Git history → coi như đã lộ.
Quy tắc bắt buộc
– Không commit .env.
– Không COPY . . một cách mù quáng nếu không có .dockerignore.
– Không hardcode secret trong Dockerfile.
– Inject secret lúc runtime qua biến môi trường, Docker secrets, hoặc secret manager.
Với production nhỏ trên VPS, biến môi trường runtime là mức tối thiểu chấp nhận được. Nhưng hãy:
– Phân quyền file chứa env nếu dùng env_file.
– Rotate secret định kỳ.
– Tách secret theo môi trường: dev/staging/prod.
6. Network: chỉ expose cái cần expose
Trong Docker, EXPOSE 3000 chỉ là metadata. Cái quyết định thật sự là publish port kiểu -p.
Khuyến nghị thực chiến
– Chỉ publish app public qua reverse proxy như Nginx hoặc Traefik.
– App NodeJS không nhất thiết phải mở thẳng ra Internet.
– DB/Redis chỉ nên nằm ở network nội bộ.
Ví dụ:
– Nginx public: 80, 443
– Node app: chỉ join Docker network, không publish public port
– PostgreSQL/Redis: nội bộ hoàn toàn
Lợi ích:
– Reverse proxy xử lý TLS, rate limiting, header bảo mật.
– App không lộ port trực tiếp.
– Kiến trúc dễ kiểm soát hơn.
7. Reverse proxy + TLS: lớp phòng thủ bắt buộc
Production thực chiến gần như luôn cần reverse proxy phía trước NodeJS.
Vì sao quan trọng?
– App lỗi memory leak → không ăn hết RAM VPS.
– Fork bomb hoặc tiến trình bất thường → bị chặn bởi pids_limit.
– Khai thác privilege escalation → khó hơn với no-new-privileges.
9. Supply chain: dependency an toàn hơn mới là bảo mật thật
Nhiều vụ tấn công không vào server trước, mà vào package. NodeJS đặc biệt nhạy điểm này vì hệ sinh thái dependency lớn.
Thực hành nên có
– Khóa version bằng package-lock.json.
– Chỉ dùng package thực sự cần.
– Chạy npm audit định kỳ, nhưng đừng fix mù quáng.
– Scan image bằng trivy hoặc công cụ tương đương.
– Theo dõi CVE từ base image và package chính.
Ví dụ:
npm audit
trivy image my-node-app:latest
Scan không thay thế hardening, nhưng giúp phát hiện sớm lỗ hổng đã biết.
10. Log, backup, cập nhật: phần hay bị quên nhất
Hệ thống “an toàn” nhưng không có log, không backup, không patch → sớm muộn cũng gặp sự cố nghiêm trọng.
Với log
– Không log password, token, access key, cookie, OTP.
– Mask dữ liệu nhạy cảm.
– Giới hạn retention tránh đầy disk.
– Theo dõi lỗi bất thường: tăng 401, 403, 5xx, CPU spike, restart loop.
Với backup
– Backup DB định kỳ.
– Test restore, không chỉ test backup.
– Lưu bản backup ngoài VPS chính.
Với cập nhật
– Vá hệ điều hành, Docker engine, Node image định kỳ.
– Rebuild image thường xuyên để lấy bản vá từ base image.
– Không dùng tag mơ hồ như latest cho production lâu dài.
11. Checklist tối thiểu trước khi go-live
Trước khi deploy production, hãy tự hỏi:
– SSH key only chưa?
– Firewall deny by default chưa?
– Container chạy non-root chưa?
– Image multi-stage, gọn chưa?
– Không có .env trong image/Git chứ?
– DB/Redis không public chứ?
– Có HTTPS + reverse proxy chưa?
– Có healthcheck + restart policy chưa?
– Có limit RAM/CPU/PID chưa?
– Có backup + test restore chưa?
– Đã scan image/dependency chưa?
Nếu còn nhiều câu “chưa”, đừng vội gọi đó là production-ready.
Kết luận
Bảo mật khi deploy NodeJS bằng Docker trên VPS không nằm ở một “mẹo thần thánh”, mà ở kỷ luật triển khai. Mỗi cấu hình nhỏ như tắt root SSH, dùng non-root container, không nhúng secret vào image, đóng port DB, bật HTTPS, giới hạn quyền runtime — tự nó có thể trông đơn giản. Nhưng ghép lại, chúng tạo khác biệt rất lớn giữa một hệ thống “chạy được” và một hệ thống “chịu được đòn”.
Nếu bạn muốn ưu tiên theo thứ tự, hãy làm 5 việc đầu tiên: harden VPS, non-root container, secret đúng cách, reverse proxy + HTTPS, đóng network tối đa. Chỉ 5 bước này đã loại bỏ phần lớn lỗi bảo mật phổ biến trong các ca deploy NodeJS bằng Docker trên VPS thực chiến.
Muốn an toàn thật, đừng hỏi “làm sao để không bị hack”. Hãy hỏi: nếu bị chọc thủng một lớp, hệ thống của mình còn giữ được gì? Đó mới là tư duy của production thực chiến.