Deploy Node.js lên VPS bằng PM2 và Nginx từ A–Z
Hướng dẫn deploy Node.js lên VPS Ubuntu bằng PM2 và Nginx: build, biến môi trường, reverse proxy, HTTPS, log, healthcheck và rollback.
Bài viết bởi Phạm Minh Thiện
Front-end Developer, trực tiếp thực hành Next.js, NestJS, Docker, Nginx và VPS Ubuntu.
Bài viết đã được kiểm tra lại ngày 21/7/2026.
Mục lục bài viết

Deploy Node.js lên VPS không nên dừng ở lệnh npm start. Trong production, ứng dụng cần được build đúng phiên bản Node.js, chạy dưới process manager, tự khởi động sau reboot, có log, healthcheck và được Nginx bảo vệ phía trước.
Hướng dẫn này triển khai một ứng dụng Node.js trên Ubuntu bằng PM2 và Nginx theo luồng thực tế: chạy thử upstream, tạo ecosystem file, cấu hình reverse proxy, kiểm tra lỗi EADDRINUSE, thiết lập startup, rollback và xác minh sau deploy.
Tóm tắt nhanh: Luôn chạy ứng dụng trực tiếp thành công trước, sau đó mới đưa vào PM2; gọi
curl 127.0.0.1:PORTtrước khi cấu hình Nginx và chỉ mở cổng 80/443 ra Internet.

Demo terminal: xác minh PM2 và upstream trước khi mở domain
Nếu upstream chưa phản hồi, chỉnh Nginx sẽ không giải quyết được vấn đề. Demo này kiểm tra build, trạng thái PM2, cổng đang listen và health endpoint trước khi reload Nginx.

pm2 status
pm2 logs deployeasy-api --lines 80
ss -lntp | grep :3001
curl -fsS http://127.0.0.1:3001/health
sudo nginx -t && sudo systemctl reload nginx
Lưu ý: Output trong ảnh và ví dụ là demo lab đã ẩn thông tin nhạy cảm. Khi chụp terminal thật, hãy che IP quản trị, username nội bộ, token, private key, mật khẩu và chuỗi kết nối database.
Khi nào nên dùng PM2?
PM2 phù hợp khi:
- Deploy Node.js trực tiếp trên VPS.
- Không dùng Docker.
- Cần quản lý process đơn giản.
- Cần restart khi app crash.
- Có một hoặc vài app.
Không cần PM2 nếu app đã được Docker, systemd hoặc nền tảng managed quản lý.
Chuẩn bị server
Ubuntu VPS
User sudo
SSH key
Node.js đúng phiên bản
Git
Nginx
Domain đã trỏ
Kiểm tra:
node --version
npm --version
git --version
nginx -v
Cài Node.js
Dùng phương thức quản lý version rõ ràng. Nếu dùng NVM:
nvm install --lts
nvm use --lts
node --version
Khi nâng Node.js với NVM, có thể cần tạo lại PM2 startup command để PATH đúng.
Cài PM2
npm install -g pm2
pm2 --version
PM2 là daemon process manager và cung cấp CLI để quản lý ứng dụng.
Chuẩn bị thư mục
sudo mkdir -p /var/www/deployeasy-api
sudo chown -R "$USER":"$USER" /var/www/deployeasy-api
cd /var/www/deployeasy-api
Clone:
git clone git@github.com:USERNAME/REPOSITORY.git .
Cài dependency
npm ci
Nếu cần build:
npm ci
npm run build
Chỉ prune dev dependencies khi chắc runtime không cần chúng:
npm prune --omit=dev
Biến môi trường
Tạo:
nano .env.production
chmod 600 .env.production
Ví dụ:
NODE_ENV=production
PORT=3001
DATABASE_URL=postgresql://...
JWT_SECRET=...
Không commit file này.
Chạy thử trước PM2
npm run build
npm run start
Terminal khác:
curl -I http://127.0.0.1:3001
Chỉ dùng PM2 khi app chạy trực tiếp thành công.
Chạy bằng PM2
NestJS:
pm2 start dist/main.js --name deployeasy-api
Next.js:
pm2 start npm --name deployeasy-web -- start
Kiểm tra:
pm2 list
pm2 describe deployeasy-api
pm2 logs deployeasy-api
Ecosystem file
Tạo:
nano ecosystem.config.cjs
module.exports = {
apps: [
{
name: "deployeasy-api",
cwd: "/var/www/deployeasy-api",
script: "dist/main.js",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
PORT: 3001,
},
max_memory_restart: "500M",
restart_delay: 3000,
time: true,
error_file: "/var/log/pm2/deployeasy-api-error.log",
out_file: "/var/log/pm2/deployeasy-api-out.log",
merge_logs: true,
},
],
};
Tạo thư mục log:
sudo mkdir -p /var/log/pm2
sudo chown -R "$USER":"$USER" /var/log/pm2
Start:
pm2 start ecosystem.config.cjs
Cập nhật env:
pm2 restart ecosystem.config.cjs --update-env
Vì sao dùng .cjs?
Nếu package.json có:
{
"type": "module"
}
file .js được hiểu là ESM, nhưng module.exports là CommonJS. .cjs tránh xung đột.
Startup sau reboot
pm2 startup
PM2 in ra một lệnh có sudo và PATH. Copy đúng lệnh đó.
Lưu process:
pm2 save
Reboot test:
sudo reboot
SSH lại:
pm2 list
Lệnh PM2 quan trọng
pm2 list
pm2 start ecosystem.config.cjs
pm2 restart deployeasy-api
pm2 reload deployeasy-api
pm2 stop deployeasy-api
pm2 delete deployeasy-api
pm2 logs deployeasy-api
pm2 monit
pm2 save
Không mặc định coi reload là zero downtime cho mọi ứng dụng.
Cluster mode
{
name: "deployeasy-api",
script: "dist/main.js",
instances: "max",
exec_mode: "cluster"
}
Chỉ dùng khi:
- App stateless.
- Session không lưu trong RAM local.
- Cron/job không chạy lặp ở mọi worker.
- VPS có nhiều CPU.
- Đã load test.
VPS 1 vCPU thường không cần cluster nhiều worker.
Graceful shutdown
Node.js:
process.on("SIGTERM", async () => {
await app.close();
process.exit(0);
});
NestJS:
app.enableShutdownHooks();
Cần đóng database connection, queue consumer và background task.
Nginx reverse proxy
server {
listen 80;
listen [::]:80;
server_name api.deployeasy.vn;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Kiểm tra:
sudo nginx -t
sudo systemctl reload nginx
Health endpoint
Tạo:
GET /health
Response:
{
"status": "ok"
}
Kiểm tra:
curl -f http://127.0.0.1:3001/health
curl -f https://api.deployeasy.vn/health
Deploy thủ công
cd /var/www/deployeasy-api
git fetch origin
git checkout main
git pull --ff-only origin main
npm ci
npm run build
pm2 restart ecosystem.config.cjs --update-env
curl -f http://127.0.0.1:3001/health
Nhược điểm: build thất bại có thể để working directory ở trạng thái không mong muốn.
Deploy theo release
/var/www/deployeasy-api/
├── releases/
│ ├── 20260710-100000/
│ └── 20260710-110000/
├── shared/
│ └── .env.production
└── current -> releases/20260710-110000
Quy trình:
- Tạo release mới.
- Copy artifact.
- Cài dependency.
- Build.
- Symlink env.
- Test.
- Chuyển
current. - Restart/reload.
- Giữ vài release cũ.
Rollback:
ln -sfn /var/www/deployeasy-api/releases/RELEASE_CU \
/var/www/deployeasy-api/current
pm2 restart deployeasy-api
Database migration vẫn cần chiến lược riêng.
Log PM2
pm2 logs
pm2 logs deployeasy-api --lines 200
Cài log rotation:
pm2 install pm2-logrotate
Kiểm tra cấu hình theo tài liệu PM2 hiện hành.
Lỗi EADDRINUSE
Error: listen EADDRINUSE
Tìm process:
sudo ss -ltnp | grep :3001
sudo lsof -i :3001
Có thể app cũ vẫn chạy ngoài PM2 hoặc hai process cùng dùng port.
Lỗi thiếu build
Next.js:
npm run build
npm run start
NestJS:
npm run build
node dist/main.js
Kiểm tra script và output thật, không giả định luôn là dist/main.js.
Exit code 137
Kiểm tra:
free -h
dmesg --ctime | tail -n 50
Có thể process bị OOM. Giải pháp:
- Tăng RAM.
- Tạo swap hỗ trợ.
- Build trong CI.
- Deploy artifact.
- Tối ưu build.
PM2 phải chạy đúng user
Không nên chạy:
sudo pm2 start ...
rồi dùng PM2 bằng user thường. Root và user có process list khác nhau.
pm2 report
Dùng một user deploy rõ ràng.
Rollback
Ghi commit trước deploy:
git rev-parse HEAD
Rollback:
git checkout COMMIT_CU
npm ci
npm run build
pm2 restart deployeasy-api
Tốt hơn là dùng release hoặc artifact có version.
Migration database phá vỡ tương thích có thể làm code cũ không chạy được.
Checklist
[ ] Node.js đúng version
[ ] npm ci thành công
[ ] Build thành công
[ ] Start trực tiếp hoạt động
[ ] PM2 chạy đúng user
[ ] Ecosystem file rõ ràng
[ ] pm2 startup
[ ] pm2 save
[ ] Log rotation
[ ] Health endpoint
[ ] Nginx đúng port
[ ] App bind localhost
[ ] HTTPS hoạt động
[ ] Có rollback
[ ] Migration được đánh giá
Câu hỏi thường gặp
PM2 có thay Nginx không?
Không. PM2 quản lý process; Nginx xử lý HTTP/HTTPS phía trước.
PM2 có tự build source không?
Không. Bạn phải chạy build hoặc viết script deploy.
PM2 có dùng được cho Next.js?
Có thể chạy npm start, nhưng Vercel hoặc container cũng là lựa chọn.
PM2 có bảo đảm app luôn online không?
Không. Server, database, network hoặc bug vẫn có thể làm dịch vụ down.
Bài viết liên quan
- Cấu hình VPS Ubuntu mới
- Nginx Reverse Proxy là gì?
- Sửa lỗi Nginx 502 Bad Gateway
- GitHub Actions tự động deploy VPS
Kết luận
PM2 phù hợp cho Node.js trên VPS không dùng Docker. Production vẫn cần Nginx, HTTPS, healthcheck, log rotation và rollback.
Nguồn tham khảo
Đọc tiếp
Bài viết liên quan
Cách trỏ tên miền về VPS bằng DNS, Cloudflare và Nginx
Hướng dẫn trỏ tên miền về VPS bằng A, AAAA, CNAME; kiểm tra DNS propagation, cấu hình Cloudflare, Nginx và xử lý lỗi www.
Đọc bài →Monitoring VPS production: Log, CPU, RAM, disk và healthcheck
Hướng dẫn monitoring VPS production bằng journalctl, Nginx log, PM2, Docker healthcheck; theo dõi CPU, RAM, disk, uptime và cảnh báo.
Đọc bài →GitHub Actions CI/CD: Tự động deploy Node.js lên VPS
Tạo GitHub Actions CI/CD để build, test và tự động deploy Node.js lên VPS qua SSH an toàn, có secrets, healthcheck, concurrency và rollback.
Đọc bài →