DeployEasy
ProductionTrung bình

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.

· 7 phút đọc· 1.473 từ
Mục lục bài viết

Luồng deploy Node.js lên VPS bằng PM2 và Nginx

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:PORT trước khi cấu hình Nginx và chỉ mở cổng 80/443 ra Internet.

Sơ đồ luồng cho deploy Node.js lên VPS

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.

Terminal demo cho deploy Node.js lên VPS

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:

  1. Tạo release mới.
  2. Copy artifact.
  3. Cài dependency.
  4. Build.
  5. Symlink env.
  6. Test.
  7. Chuyển current.
  8. Restart/reload.
  9. 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

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