Deploy NodeJS bằng Docker trên VPS: những lỗi rất hay gặp, vì sao xảy ra, cách xử lý gọn mà đúng
Deploy NodeJS lên VPS bằng Docker nghe rất “êm”: build image, chạy container, map port, xong. Thực tế khác hẳn. App chạy tốt ở local nhưng lên VPS lại lỗi ngầm: container restart liên tục, app không truy cập được, build fail vì package native, memory tăng dần rồi chết, log im lặng đúng lúc cần nhất.
Điểm khó nằm ở chỗ: Docker, NodeJS, Linux VPS, reverse proxy, network, volume, quyền file, biến môi trường — mọi thứ liên kết nhau. Sai một mắt xích nhỏ → downtime, tốn giờ debug.
Bài này tập trung vào những lỗi phổ biến nhất khi deploy NodeJS bằng Docker trên VPS, kèm dấu hiệu nhận biết, nguyên nhân gốc, cách xử lý thực tế. Mục tiêu: giúp bạn không chỉ “chữa cháy”, mà còn dựng được quy trình deploy ổn định hơn.
1. App chạy trong container nhưng không truy cập được từ bên ngoài
Dấu hiệu
– docker ps thấy container đang chạy
– Log app báo Server listening on 3000
– Truy cập IP hoặc domain trên VPS → timeout / connection refused
Nguyên nhân thường gặp
Bind sai host → app chỉ lắng nghe 127.0.0.1 trong container, không phải toàn bộ interface.
Ví dụ sai:
app.listen(3000, '127.0.0.1')
Trong container, cấu hình này khiến app chỉ mở cổng nội bộ cục bộ → Docker không forward đúng ra ngoài.
Cách xử lý
Cho app bind 0.0.0.0:
app.listen(process.env.PORT || 3000, '0.0.0.0')
Hoặc đơn giản:
app.listen(process.env.PORT || 3000)
Kèm kiểm tra mapping port:
docker run -p 3000:3000 my-node-app
Nếu có UFW/firewalld trên VPS, mở port tương ứng:
sudo ufw allow 3000
Nếu đi qua Nginx/Caddy → kiểm tra reverse proxy đang trỏ đúng localhost:3000 hay container network tương ứng.
2. Build Docker image thất bại vì node_modules, package native, khác môi trường
Dấu hiệu
– Build fail ở bước npm install hoặc npm ci
– Lỗi kiểu:
– node-gyp rebuild failed
– python not found
– g++ not found
– Unsupported platform
– ELFCLASS64 / binary incompatibility
Nguyên nhân
NodeJS thường dùng package native như bcrypt, sharp, canvas, sqlite3, puppeteer. Chúng phụ thuộc OS, libc, compiler, system library. Local build trên macOS/Windows rồi copy node_modules vào image Linux → rất dễ vỡ.
Cách xử lý
Không copy node_modules từ local vào container. Dùng .dockerignore:
node_modules
npm-debug.log
.git
.env
Dockerfile nên cài dependency ngay trong image:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
Lưu ý:alpine nhẹ nhưng nhiều package native lỗi hơn vì dùng musl thay glibc. Nếu gặp lỗi khó chịu với sharp, canvas, puppeteer → thử chuyển sang node:20-slim.
3. Container chạy rồi tự thoát ngay
Dấu hiệu
– docker ps không thấy container sống lâu
– docker ps -a thấy status Exited
– Log rất ngắn hoặc không có gì rõ ràng
Nguyên nhân
Container sống theo process chính. Process chính kết thúc → container dừng. Hay gặp khi:
– Sai CMD
– App crash ngay lúc start
– Script start chỉ chạy lệnh ngắn rồi thoát
– Thiếu biến môi trường bắt buộc
Cách xử lý
Xem log trước:
docker logs <container_id>
Kiểm tra Dockerfile:
CMD ["node", "server.js"]
Tránh kiểu shell script mơ hồ nếu không cần.
Nếu app phụ thuộc env, xác nhận .env hoặc biến runtime đã truyền đúng:
docker run --env-file .env -p 3000:3000 my-node-app
Dùng restart policy để tránh phải khởi động thủ công sau lỗi tạm thời:
docker run -d --restart unless-stopped -p 3000:3000 my-node-app
Nhưng nhớ: restart policy chỉ che triệu chứng. Gốc vẫn phải xem log.
4. Dùng sai biến môi trường, config production bị lệch
Dấu hiệu
– App connect DB fail trên VPS, local vẫn ổn
– JWT secret thiếu
– App chạy nhầm chế độ development
– CORS, cookie, URL callback lỗi
Nguyên nhân
Config local thường “dễ tính”. Lên VPS mới lộ ra:
– Quên mount .env
– Sai tên biến
– Hardcode localhost
– Dùng NODE_ENV=development ở production
Cách xử lý
Chuẩn hóa config theo nguyên tắc: fail fast. Thiếu env quan trọng → app dừng ngay, báo rõ.
Ví dụ:
const required = ['PORT', 'DATABASE_URL', 'JWT_SECRET']
for (const key of required) {
if (!process.env[key]) {
throw new Error(Missing env: ${key})
}
}
Không hardcode URL nội bộ. Với Docker Compose, dùng tên service thay vì localhost cho kết nối giữa container:
– Sai: localhost:5432
– Đúng: postgres:5432
localhost trong container nghĩa là chính container đó, không phải VPS host, cũng không phải container DB khác.
5. Lỗi permission với volume, upload, log, cache
Dấu hiệu
– App không ghi được file upload
– Lỗi EACCES: permission denied
– Cache/session/file-based DB không hoạt động
Nguyên nhân
Mount volume từ host vào container nhưng user trong container không có quyền ghi. Rất hay gặp khi app chạy non-root hoặc image base có user riêng.
Cách xử lý
Kiểm tra quyền thư mục trên VPS:
ls -lah
Tạo thư mục đúng quyền:
mkdir -p uploads
chmod 775 uploads
Trong Dockerfile, chủ động set owner nếu cần:
RUN mkdir -p /app/uploads && chown -R node:node /app
USER node
Nếu dùng bind mount:
docker run -v $(pwd)/uploads:/app/uploads my-node-app
→ đảm bảo UID/GID tương thích. Đừng chmod 777 mọi thứ như một “giải pháp”.
6. App ngốn RAM, VPS swap mạnh, container bị kill
Dấu hiệu
– App chậm dần
– VPS đơ
– Container bị dừng đột ngột
– docker logs có thể không rõ
– dmesg thấy OOM kill
Nguyên nhân
NodeJS giữ memory cho V8; app có memory leak; log quá nhiều; xử lý file lớn; VPS quá ít RAM; chạy nhiều container chung máy.
Cách xử lý
Giới hạn tài nguyên container:
docker run -d --memory="512m" --cpus="1.0" my-node-app
Nếu app hợp lệ nhưng V8 cần cấu hình:
node --max-old-space-size=384 server.js
Theo dõi RAM:
docker stats
free -m
Tối ưu thêm:
– Không giữ object lớn quá lâu
– Stream file thay vì load hết vào memory
– Dùng PM2 trong container chỉ khi thật cần; đa số trường hợp 1 process/container là đủ
– Tách worker/background job khỏi web app nếu tải lớn
7. Log khó đọc, không có dữ liệu để debug
Dấu hiệu
– App lỗi nhưng không thấy gì trong docker logs
– Log chỉ có startup message
– Muốn truy log file trong container nhưng file mất sau redeploy
Nguyên nhân
Ghi log vào file nội bộ container → không bền. Docker chuẩn nhất là ghi ra stdout/stderr.
Cách xử lý
Trong NodeJS, log ra console:
console.log('App started')
console.error(err)
Xem log:
docker logs -f <container_id>
Nếu cần tập trung log:
– Dùng Nginx + Docker logs
– Hoặc đẩy sang Loki, ELK, Datadog, Better Stack
Thêm healthcheck để phát hiện app “sống giả chết thật”:
8. Deploy xong downtime vì build trực tiếp trên VPS, rollback khó
Dấu hiệu
– Mỗi lần deploy phải SSH vào server, pull code, build tay
– Build lỗi giữa chừng → app cũ đã dừng
– Không có bản rollback rõ ràng
Nguyên nhân
Quy trình deploy thủ công, thiếu image versioning, thiếu reverse proxy ổn định, thiếu tách biệt build/run.
Cách xử lý
Thực tế nhất:
1. Build image ở CI hoặc local sạch
2. Tag version rõ ràng: my-app:1.4.2
3. Push registry
4. VPS chỉ pull + restart container
5. Giữ image cũ để rollback nhanh
Điểm này cực quan trọng với auth, session, OAuth callback, secure cookie.
Kết luận: deploy ổn định không nằm ở “chạy được”, mà ở “chạy đúng lâu dài”
Deploy NodeJS bằng Docker trên VPS không khó ở lệnh docker run, mà khó ở các chi tiết môi trường thật: network, config, quyền file, reverse proxy, memory, log, quy trình cập nhật. Phần lớn sự cố production đến từ vài lỗi lặp đi lặp lại:
– bind sai host
– dùng sai env
– copy node_modules từ local
– không hiểu localhost trong container
– thiếu quyền volume
– không theo dõi RAM/log
– deploy thủ công, không version image
Nếu muốn giảm lỗi rõ rệt, hãy áp dụng bộ nguyên tắc ngắn gọn này: image sạch, config tường minh, log ra stdout, healthcheck rõ, reverse proxy chuẩn, resource limit hợp lý, deploy có version và rollback.
Làm được vậy → VPS nhỏ vẫn chạy ổn, debug nhanh hơn, downtime ít hơn nhiều. Nếu bạn đang deploy app thật cho khách hàng hoặc sản phẩm nội bộ, đó không còn là “tối ưu”, mà là mức tối thiểu nên có.