Deploy Node.js lên VPS Ubuntu không khó nếu bạn đi đúng thứ tự: cài Node.js bản LTS, chạy app bằng PM2 để app luôn sống, rồi đặt Nginx làm reverse proxy phía trước để nhận tên miền và SSL. Bài này hướng dẫn từng bước trên Ubuntu 22.04 và 24.04, áp dụng cho Express, NestJS, Fastify hay Next.js chạy chế độ server.

Chuẩn bị trước khi bắt đầu

  • Một VPS Ubuntu 22.04 hoặc 24.04, có quyền sudo. Nếu chưa có, bạn có thể tham khảo VPS giá rẻ của TDCloud hoặc so sánh thêm các gói tại danh sách VPS.
  • Một tên miền đã trỏ bản ghi A về IP của VPS. Xem cách làm trong bài trỏ tên miền về VPS.
  • Mã nguồn app trên Git (GitHub, GitLab) và app đọc cổng từ biến môi trường PORT.
  • Đã đăng nhập SSH bằng key và làm các bước bảo mật cơ bản như trong bài bảo mật VPS Ubuntu 24.04.

Trong bài, tên miền mẫu là example.com, app tên myapp, chạy ở cổng 3000. Hãy thay bằng thông tin của bạn.

Bước 1: Tạo user riêng để chạy app

Không nên chạy app Node.js bằng root. Tạo một user riêng, ví dụ deploy:

sudo adduser deploy
sudo usermod -aG sudo deploy
su - deploy

Từ đây, các lệnh chạy bằng user deploy.

Bước 2: Cài Node.js LTS từ NodeSource

Kho mặc định của Ubuntu thường có Node.js khá cũ. Cách phổ biến là dùng kho NodeSource. Theo nodejs.org tại thời điểm viết, Node.js 24 là bản LTS mới nhất, nên ta cài nhánh 24:

sudo apt update
sudo apt install -y curl
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -v
npm -v

Lệnh node -v phải trả về phiên bản bắt đầu bằng v24. Gói nodejs của NodeSource đã kèm sẵn npm.

Nếu cần chạy nhiều phiên bản Node.js trên cùng VPS, bạn có thể dùng nvm thay cho NodeSource. Khi đó lưu ý đường dẫn Node nằm trong thư mục home, phần pm2 startup bên dưới sẽ tự xử lý việc này.

Bước 3: Cài Git, Nginx và PM2

sudo apt install -y git nginx
sudo npm install -g pm2
pm2 -v

Bước 4: Lấy mã nguồn và cài thư viện

mkdir -p ~/apps
cd ~/apps
git clone https://github.com/ten-ban/myapp.git
cd myapp
npm ci --omit=dev

npm ci cài đúng phiên bản trong package-lock.json, phù hợp cho production. Tham số --omit=dev bỏ qua thư viện chỉ dùng khi phát triển. Nếu app cần bước build (TypeScript, Next.js), hãy chạy npm ci đầy đủ, rồi npm run build.

Đặt các secret như chuỗi kết nối database, khóa API vào file .env hoặc vào file cấu hình PM2 bên dưới. Không commit các giá trị này lên Git.

Bước 5: Chạy app bằng PM2 với file ecosystem

PM2 có thể tạo file mẫu bằng lệnh pm2 init simple. Bạn cũng có thể tự tạo file ecosystem.config.js trong thư mục app. Tài liệu PM2 lưu ý tên file tự viết phải kết thúc bằng .config.js.

module.exports = {
  apps: [{
    name: "myapp",
    script: "./server.js",
    instances: 1,
    exec_mode: "fork",
    max_memory_restart: "300M",
    env_production: {
      NODE_ENV: "production",
      PORT: 3000
    }
  }]
}

Ý nghĩa các thuộc tính chính:

  • script: file khởi động app, đường dẫn tính từ thư mục bạn chạy pm2 start.
  • instances và exec_mode: mặc định là fork, một tiến trình. Chế độ cluster chạy nhiều tiến trình; đặt instances: 0 để chạy bằng số CPU.
  • max_memory_restart: tự khởi động lại app khi vượt mức RAM chỉ định.
  • env_production: biến môi trường chỉ áp dụng khi chạy với --env production.

Khởi động và kiểm tra:

pm2 start ecosystem.config.js --env production
pm2 status
pm2 logs myapp --lines 50
curl -I http://127.0.0.1:3000

Nếu curl trả về mã HTTP (200, 301, 404...), app đã chạy. Nếu lỗi, đọc log bằng pm2 logs.

Bước 6: Tự chạy app khi VPS khởi động lại

pm2 startup

PM2 sẽ in ra một lệnh bắt đầu bằng sudo env PATH=$PATH:.... Hãy sao chép đúng lệnh đó và chạy. Sau đó lưu danh sách app đang chạy:

pm2 save

Thử sudo reboot, đăng nhập lại và chạy pm2 status để chắc chắn app tự lên. Khi nâng cấp Node.js về sau, chạy pm2 unstartup, rồi pm2 startup và pm2 save lại để PM2 trỏ đúng file node mới.

Bước 7: Cấu hình Nginx reverse proxy

App đang chạy ở 127.0.0.1:3000, chỉ truy cập được từ trong VPS. Nginx sẽ nhận request từ Internet ở cổng 80/443 rồi chuyển vào app. Tạo file cấu hình:

sudo nano /etc/nginx/sites-available/myapp

Dán nội dung sau:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        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;
    }
}

Hai header Upgrade và Connection cần cho WebSocket (Socket.IO, ứng dụng chat, realtime). Tài liệu Nginx giải thích đây là header hop-by-hop, Nginx không tự chuyển tiếp nếu không khai báo. Dòng proxy_http_version 1.1 cần cho các bản Nginx cũ hơn 1.29.7, gồm bản trong kho Ubuntu 22.04 và 24.04.

Khối map chỉ nên khai báo một lần. Nếu VPS chạy nhiều site Node.js, hãy chuyển khối này sang một file riêng, ví dụ /etc/nginx/conf.d/websocket-map.conf, để tránh lỗi khai báo trùng.

Kích hoạt site và kiểm tra cú pháp:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Vì app đứng sau proxy, hãy để app đọc IP thật của khách từ header X-Forwarded-For. Với Express, thêm app.set('trust proxy', 1) để req.ip lấy IP khách và req.protocol nhận đúng HTTPS từ header X-Forwarded-Proto. Chỉ dùng giá trị 1 khi app luôn đứng sau đúng một proxy là Nginx.

Bước 8: Mở firewall và cài SSL

sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
sudo ufw status

Không mở cổng 3000 ra ngoài. App chỉ cần nghe ở 127.0.0.1. Tiếp theo, cài chứng chỉ Let's Encrypt bằng Certbot theo bài cài SSL Let's Encrypt cho Nginx. Certbot sẽ tự sửa file cấu hình ở bước 7 để thêm cổng 443 và chuyển hướng HTTP sang HTTPS.

Bước 9: Cập nhật code về sau

Mỗi lần có phiên bản mới, quy trình cơ bản là:

cd ~/apps/myapp
git pull
npm ci --omit=dev
pm2 reload ecosystem.config.js --env production

Theo tài liệu PM2, ở chế độ cluster, pm2 reload khác restart ở chỗ không tắt toàn bộ app cùng lúc, giúp tránh gián đoạn. Nếu reload không thành công trong thời gian chờ, PM2 sẽ quay về restart thông thường. Muốn reload mượt, app nên bắt tín hiệu SIGINT để xử lý xong request dở dang trước khi thoát. Nếu app có bước build, chạy build trước khi reload.

Quản lý log và theo dõi

  • pm2 logs myapp: xem log trực tiếp.
  • pm2 monit: xem CPU, RAM của từng app.
  • pm2 install pm2-logrotate: cài module xoay vòng log, tránh file log làm đầy ổ đĩa.
  • sudo tail -f /var/log/nginx/error.log: xem lỗi phía Nginx, ví dụ lỗi 502 khi app chưa chạy.

Lỗi thường gặp

502 Bad Gateway

Nginx không kết nối được tới app. Kiểm tra pm2 status, xem app có đang chạy và có đúng cổng 3000 không. Lệnh curl -I http://127.0.0.1:3000 giúp xác định lỗi nằm ở app hay ở Nginx.

App tắt sau khi reboot

Thường do quên chạy lệnh mà pm2 startup in ra, hoặc quên pm2 save. Làm lại bước 6.

WebSocket bị ngắt sau khoảng 60 giây

Theo tài liệu Nginx, kết nối proxy mặc định đóng nếu backend không gửi dữ liệu trong 60 giây. Tăng proxy_read_timeout trong khối location, hoặc cho app gửi ping định kỳ.

App ăn hết RAM

Đặt max_memory_restart hợp lý, kiểm tra rò rỉ bộ nhớ trong code, và cân nhắc tạo swap cho VPS ít RAM.

Nguồn tham khảo