Deploy Node.js production bằng PM2 và Nginx trên VPS
Hướng dẫn deploy ứng dụng Node.js bằng PM2, ecosystem file, biến môi trường, startup, log, Nginx reverse proxy và rollback.
Mục lục bài viết
VPS & Hosting cho dự án của bạn
Khám phá các gói máy chủ và lưu trữ phù hợp từ website cá nhân đến ứng dụng production.
PM2 là process manager cho Node.js. Nó giúp start, stop, restart, xem log và lưu danh sách process để khởi động lại sau khi VPS reboot.
Internet
↓
Nginx :80/:443
↓
PM2
↓
Node.js :3000
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.
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
Domain và DNS là gì? Cách trỏ tên miền về VPS đúng cách
Giải thích domain, DNS, nameserver, bản ghi A, AAAA, CNAME, TXT và quy trình trỏ tên miền về VPS để deploy website.
Đọc bài →Bảo mật PostgreSQL trên VPS: pg_hba.conf, user và firewall
Hướng dẫn bảo mật PostgreSQL production bằng listen_addresses, pg_hba.conf, role tối thiểu, password, firewall, SSL và SSH tunnel.
Đọc bài →Backup và restore PostgreSQL production bằng pg_dump
Hướng dẫn backup PostgreSQL bằng pg_dump, định dạng custom, pg_restore, lịch cron, kiểm tra restore và lưu bản sao ngoài VPS.
Đọc bài →