DeployEasy
Menu
ProductionTrung bình

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.

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

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:

  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.

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