Deploy Node.js lên VPS: hướng dẫn từ A-Z với nginx, SSL và systemd

Cách deploy Node.js lên VPS Ubuntu/Debian: cài Node LTS, chạy bằng systemd, nginx reverse proxy, SSL miễn phí và cập nhật app không gián đoạn.

Sơ đồ: trình duyệt, nginx với SSL và ứng dụng Node.js chạy trên VPS

Ý chính

  • Ứng dụng Node.js nên lắng nghe trên 127.0.0.1 và để nginx lo tên miền, SSL và file tĩnh.
  • systemd đủ để giữ app luôn chạy và tự khởi động lại, không bắt buộc phải có PM2.
  • Gói nodejs có sẵn trong Ubuntu 24.04 và Debian 12 là Node 18, đã hết hỗ trợ; hãy dùng bản LTS 22 hoặc 24.
  • Muốn cập nhật không gián đoạn, bản mới phải khởi động và qua health check trước khi nhận traffic.

Để deploy Node.js lên VPS, bạn cần bốn thứ: một bản Node.js LTS còn được hỗ trợ, một trình quản lý tiến trình giữ app luôn chạy (systemd hoặc PM2), nginx làm reverse proxy trước app, và chứng chỉ SSL cho tên miền. App chỉ lắng nghe trên 127.0.0.1, còn nginx nhận request từ internet ở cổng 80/443 rồi chuyển vào. Làm tay mất khoảng 30-60 phút cho lần đầu; với control panel có hỗ trợ ứng dụng Node.js thì chỉ vài phút.

Bài này đi qua cả hai cách: làm thủ công trên Ubuntu 24.04 hoặc Debian 12 (từng lệnh, kèm lý do), và cách làm với ZoPanel. Cuối bài là các lỗi hay gặp, cách cập nhật không gián đoạn và phần câu hỏi thường gặp.

Cần chuẩn bị gì trước khi deploy Node.js lên VPS?

  • Một VPS Ubuntu 24.04 hoặc Debian 12/13 có quyền root hoặc sudo. Với một app Express hay NestJS nhỏ, 1 GB RAM là chạy được; nếu app cần build Next.js ngay trên server thì nên có 2 GB trở lên, vì bước build thường tốn RAM hơn lúc chạy.
  • Một tên miền đã trỏ bản ghi A về IP của VPS. Nếu chưa làm, xem bài trỏ tên miền về VPS.
  • Mã nguồn trên GitHub/GitLab hoặc trên máy bạn, có lệnh start chạy được ở môi trường production.
  • App đọc cổng từ biến môi trường PORT thay vì viết cứng. Đây là thói quen nhỏ nhưng giúp mọi cách deploy đều dễ hơn.

Nếu VPS mới tinh, hãy làm phần bảo mật cơ bản trước (SSH key, tường lửa, cập nhật tự động). Bài bảo mật VPS Ubuntu có danh sách đầy đủ.

Nên dùng phiên bản Node.js nào?

Hãy dùng bản LTS đang được hỗ trợ. Theo lịch phát hành của Node.js, tại thời điểm viết (tháng 10/2026) Node.js 24 và 22 là LTS, còn Node.js 20 và 18 đã hết hỗ trợ (EOL) và không còn nhận bản vá bảo mật.

Một cái bẫy hay gặp: gói nodejs có sẵn trong kho của Ubuntu 24.04 là Node 18.19, của Debian 12 là Node 18.20. Chạy apt install nodejs cho bạn một phiên bản đã hết hỗ trợ. Vì thế bài này cài Node.js từ bản build chính thức trên nodejs.org.

Cách deploy Node.js lên VPS thủ công

Bước 1: Cài Node.js LTS từ bản build chính thức

Lệnh dưới đây tải bản Node.js 24 mới nhất, kiểm tra checksum SHA-256 trước khi giải nén, rồi đặt vào /opt/node. Trên server ARM64, đổi linux-x64 thành linux-arm64.

cd /tmp
curl -fsSLO https://nodejs.org/dist/latest-v24.x/SHASUMS256.txt
FILE=$(grep -o 'node-v24\.[0-9.]*-linux-x64\.tar\.xz' SHASUMS256.txt | head -n1)
curl -fsSLO "https://nodejs.org/dist/latest-v24.x/$FILE"
grep " $FILE\$" SHASUMS256.txt | sha256sum -c -
sudo mkdir -p /opt/node
sudo tar -xJf "$FILE" -C /opt/node --strip-components=1
sudo ln -sf /opt/node/bin/node /usr/local/bin/node
sudo ln -sf /opt/node/bin/npm /usr/local/bin/npm
sudo ln -sf /opt/node/bin/npx /usr/local/bin/npx
node -v && npm -v

Nếu dòng sha256sum -c không in ra OK, đừng giải nén: file đã tải không đúng. Khi có bản vá mới, chạy lại đoạn trên rồi khởi động lại app. Nếu thích quản lý bằng apt, kho của NodeSource là lựa chọn phổ biến; hãy làm theo hướng dẫn thêm kho thủ công của họ thay vì chạy script tải từ internet mà chưa đọc.

Bước 2: Tạo user riêng và đưa mã nguồn lên

Đừng chạy app bằng root. Tạo một user hệ thống riêng, không có quyền đăng nhập:

sudo adduser --system --group --home /srv/myapp myapp
sudo apt install -y git
sudo -u myapp git clone https://github.com/ban/myapp.git /srv/myapp/app
cd /srv/myapp/app
sudo -u myapp npm ci --omit=dev
# Nếu app cần build (Next.js, NestJS, TypeScript):
# sudo -u myapp npm ci && sudo -u myapp npm run build

npm ci cài đúng phiên bản trong package-lock.json, nên kết quả giống hệt máy bạn. Repository riêng tư thì dùng deploy key chỉ đọc thay vì mật khẩu GitHub.

Bước 3: Chạy app bằng systemd

systemd có sẵn trên mọi bản Ubuntu/Debian, khởi động app cùng server, tự khởi động lại khi app chết và gom log vào journalctl. Đặt biến môi trường bí mật trong một file riêng chỉ root đọc được:

sudo tee /etc/myapp.env >/dev/null <<'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=mysql://myapp:matkhau@127.0.0.1/myapp
EOF
sudo chmod 600 /etc/myapp.env

Tạo file /etc/systemd/system/myapp.service:

[Unit]
Description=My Node.js app
After=network.target

[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/app
EnvironmentFile=/etc/myapp.env
ExecStart=/usr/local/bin/node server.js
Restart=on-failure
RestartSec=5
# Hạn chế quyền: app không ghi được ra ngoài thư mục được phép
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/myapp
MemoryMax=768M

[Install]
WantedBy=multi-user.target

Đổi server.js thành file khởi động của bạn (hoặc /usr/local/bin/npm run start). Rồi bật dịch vụ:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp
systemctl status myapp
journalctl -u myapp -f        # xem log trực tiếp
curl -I http://127.0.0.1:3000 # app đã trả lời chưa?

Trong code, nhớ lắng nghe trên 127.0.0.1, không phải 0.0.0.0, để app không lộ thẳng ra internet:

app.listen(process.env.PORT, "127.0.0.1");

Bước 4: Cấu hình nginx làm reverse proxy

sudo apt install -y nginx

Tạo /etc/nginx/conf.d/websocket.conf để hỗ trợ WebSocket (Socket.IO, Next.js dev, v.v.):

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

Tạo /etc/nginx/sites-available/myapp:

server {
    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 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;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}

Bật site và kiểm tra cấu hình trước khi reload, để một lỗi gõ phím không làm sập nginx:

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

Với Express, thêm app.set("trust proxy", "loopback") để req.ip và req.protocol lấy đúng IP khách và https từ header của nginx. Chi tiết các chỉ thị proxy có trong tài liệu nginx.

Bước 5: Cài SSL miễn phí và mở tường lửa

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Certbot lấy chứng chỉ Let's Encrypt, sửa cấu hình nginx để chuyển HTTP sang HTTPS và cài timer tự gia hạn. Kiểm tra gia hạn bằng sudo certbot renew --dry-run.

Nếu dùng UFW, chỉ mở SSH và web; cổng 3000 không cần mở vì app chỉ nghe nội bộ:

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

Cẩn thận: nếu SSH của bạn chạy ở cổng khác 22, hãy ufw allow <cổng>/tcp trước khi enable, kẻo tự khoá mình ngoài server.

systemd hay PM2: chọn cái nào?

Cả hai đều ổn. Khác biệt chính:

Tiêu chí systemd PM2
Cài thêm Không, có sẵn npm install -g pm2 và cần pm2 startup để chạy khi boot
Tự khởi động lại Có (Restart=on-failure) Có
Giới hạn RAM/CPU, sandbox Có (MemoryMax, ProtectSystem…) Có giới hạn RAM để restart, không có sandbox
Chạy nhiều tiến trình Tự viết unit template Cluster mode có sẵn (-i max)
Log journalctl pm2 logs, tự xoay vòng cần module thêm
Không gián đoạn khi deploy Phải tự làm pm2 reload cho cluster mode

Nếu server chỉ chạy một hai app và bạn muốn ít phụ thuộc, systemd là đủ. Nếu đã quen PM2 và cần cluster mode, PM2 rất tiện. Đừng dùng cả hai cho cùng một app.

Cập nhật app thế nào để không bị gián đoạn?

Cách đơn giản nhất là git pull, npm ci, build rồi systemctl restart myapp. Cách này gây gián đoạn vài giây, và tệ hơn: nếu bản mới lỗi khi khởi động, website chết cho tới khi bạn sửa. Có ba điểm cần lưu ý:

  1. Build trước, restart sau. Đừng build trong thư mục đang chạy; build ở thư mục release mới rồi mới chuyển sang.
  2. Kiểm tra sức khoẻ trước khi chuyển traffic. Khởi động bản mới ở cổng khác, gọi thử một route (ví dụ /health), chỉ khi trả lời đúng mới đổi proxy_pass của nginx và reload. Đây là mô hình blue/green.
  3. Giữ được bản cũ để rollback. Giữ vài thư mục release gần nhất; quay lại chỉ là trỏ về thư mục cũ.

Tự viết script cho cả ba việc là hoàn toàn làm được, nhưng đây chính là chỗ nhiều người bỏ qua cho tới ngày bản deploy lỗi lúc nửa đêm.

Deploy Node.js lên VPS bằng ZoPanel

ZoPanel làm sẵn các bước trên, gồm cả phần blue/green. Quy trình:

  1. Cài runtime: quản trị viên vào Runtime và cài Node.js. Panel cung cấp Node.js 24, 22, 20 và 18 chạy song song, kèm pnpm và yarn. Nếu dự án yêu cầu một bản chính trong .nvmrc, .node-version hoặc engines.node mà chưa cài, bản mới nhất đã cài sẽ được dùng.
  2. Tạo website: Website → Thêm website, chọn loại Git deploy, nhập địa chỉ repository, nhánh và (với monorepo) thư mục gốc. Giữ SSL miễn phí (Let's Encrypt) đang bật. Repository riêng tư thì dùng Deploy key chỉ đọc mà panel tạo cho bạn.
  3. Kiểm tra cấu hình build: ZoPanel nhận diện Next.js, Nuxt, NestJS, Express, Fastify, Koa, Hono… và đề xuất lệnh cài (npm ci, pnpm install --frozen-lockfile…), lệnh build và lệnh chạy. Bạn sửa được ở thẻ Build & chạy. Biến bí mật đặt ở Biến môi trường.
  4. Deploy: app được build bằng chính user của tài khoản hosting, trong giới hạn CPU và RAM của gói. Bản mới khởi động ở slot rảnh và phải trả lời Đường dẫn health check với mã dưới 500 trong 90 giây; chỉ khi đó nginx mới chuyển sang bản mới và bản cũ mới bị dừng. Nếu build lỗi hay app không khởi động được, bản đang chạy không bị ảnh hưởng.
  5. Tự động deploy khi push: bật Deploy khi push lên nhánh main ở thẻ Tự động deploy, rồi dán Payload URL và Secret vào webhook của GitHub, GitLab hoặc Gitea.

Một vài điểm đáng biết: app phải đọc cổng từ PORT (mỗi slot có cổng riêng, đừng viết cứng); Rollback trong thẻ Các bản release giữ 5 bản build gần nhất và cũng không gián đoạn; thẻ Dùng mọi nhân CPU chạy một tiến trình Node.js trên mỗi nhân CPU của gói mà không cần sửa code, với điều kiện app không giữ session hay websocket trong RAM. Log ứng dụng xem trực tiếp ở tab Deploy, còn lỗi 502 có nút Chạy chẩn đoán. Chi tiết trong tài liệu Ứng dụng Node.js và Python và Git deploy.

Vì mỗi app chạy như dịch vụ của một tài khoản hosting, bạn có thể chạy nhiều app của nhiều khách trên cùng VPS mà mỗi app vẫn nằm trong sandbox và giới hạn tài nguyên riêng.

Những lỗi thường gặp khi deploy Node.js

  • 502 Bad Gateway: app không chạy hoặc không nghe đúng cổng. Kiểm tra systemctl status, journalctl -u myapp và ss -ltnp | grep 3000.
  • App chạy được khi gõ tay nhưng chết dưới systemd: thường thiếu biến môi trường (systemd không đọc .bashrc) hoặc sai WorkingDirectory.
  • Build bị "Killed": hết RAM khi build. Thêm swap, build trên máy CI rồi chỉ đẩy kết quả lên, hoặc nâng RAM.
  • Đăng nhập bị lặp chuyển hướng sau khi bật HTTPS: app không tin header X-Forwarded-Proto. Bật trust proxy trong Express hoặc tuỳ chọn tương đương của framework.
  • Mở cổng 3000 ra internet: người khác truy cập thẳng app, bỏ qua SSL và mọi giới hạn ở nginx. Luôn bind 127.0.0.1.
  • Lưu file upload trong thư mục code: mất file ở lần deploy sau. Hãy để upload ở thư mục cố định ngoài release, hoặc ở kho S3.

Kết luận

Deploy Node.js lên VPS không khó: Node LTS chính thức, một user riêng, systemd, nginx và Let's Encrypt là đủ cho phần lớn ứng dụng. Phần tốn công thật sự là về sau: cập nhật không gián đoạn, rollback, log, và khi có nhiều app trên một server thì cách ly giữa chúng. Nếu bạn muốn những việc đó chạy sẵn, hãy thử gói Free của ZoPanel (3 tài khoản hosting, 10 website, 10 database, không giới hạn thời gian; Git deploy và ứng dụng Node.js đều có trong gói Free) trên một VPS thử nghiệm và deploy chính repository của bạn.

Câu hỏi thường gặp

VPS bao nhiêu RAM thì đủ để chạy Node.js?

Một app Express hay Fastify nhỏ chạy tốt trên VPS 1 GB RAM. Nếu build Next.js hoặc dự án TypeScript lớn ngay trên server, nên có 2 GB trở lên hoặc thêm swap, vì bước build tốn RAM hơn lúc chạy. Khi chạy nhiều app hay thêm database, hãy cộng RAM của từng thành phần.

Có bắt buộc dùng PM2 để chạy Node.js trên VPS không?

Không. systemd có sẵn trên Ubuntu và Debian, giữ app chạy, tự khởi động lại khi lỗi và có thêm các tuỳ chọn sandbox. PM2 tiện khi bạn cần cluster mode và lệnh pm2 reload; chọn một trong hai, đừng dùng cả hai cho cùng một app.

Vì sao không nên dùng apt install nodejs trên Ubuntu 24.04?

Vì gói trong kho mặc định của Ubuntu 24.04 là Node.js 18, và Node.js 18 đã hết hỗ trợ từ tháng 3/2025, không còn bản vá bảo mật. Hãy cài Node.js 22 hoặc 24 LTS từ bản build chính thức trên nodejs.org hoặc từ một kho apt có phiên bản mới.

Làm sao chạy nhiều ứng dụng Node.js trên một VPS?

Cho mỗi app một cổng nội bộ riêng (3000, 3001…), một dịch vụ systemd riêng và một khối server nginx riêng theo tên miền. Tốt nhất mỗi app chạy bằng một user riêng để app này không đọc được file của app kia. Control panel như ZoPanel tự cấp cổng và tách mỗi app vào tài khoản riêng.

Deploy Node.js có cần Docker không?

Không bắt buộc. Chạy trực tiếp bằng Node.js và systemd đơn giản và nhẹ hơn cho phần lớn app. Docker hữu ích khi app cần thư viện hệ thống đặc biệt hoặc bạn muốn môi trường giống hệt nhau giữa máy dev và server.