Deploy Next.js lên VPS từ A-Z, không cần Vercel

Chia sẻ bài viết

Mục lục
Deploy Next.js lên VPS từ A-Z, không cần Vercel

Trả lời nhanh: Quy trình chuẩn deploy Next.js lên VPS: build bản production (next build), chạy tiến trình bằng PM2 để tự khởi động lại khi lỗi, đặt Caddy phía trước lo tên miền và HTTPS tự động. Cập nhật phiên bản mới chỉ là git pull && npm run build && pm2 restart. Toàn bộ mất khoảng 30 phút lần đầu, và bạn thoát khỏi giới hạn của các nền tảng serverless: không giới hạn thời gian chạy, không bill bất ngờ, chạy được cron và database ngay cùng máy.

Vercel deploy Next.js một chạm rất sướng, đến khi dự án lớn lên: hàm chạy quá thời gian bị chém, băng thông vượt gói giá nhảy vọt, muốn thêm database hay cron lại phải mua dịch vụ ngoài. Deploy lên VPS của chính mình là kỹ năng đáng có: chủ động mọi thứ, chi phí cố định. Bài này là playbook đầy đủ, dán lệnh theo thứ tự là chạy.

Tóm tắt nhanh
  • Ba mảnh ghép: next build (sản phẩm) + PM2 (giữ tiến trình sống) + Caddy (tên miền, HTTPS tự động)
  • Cập nhật = git pull, build lại, pm2 restart, gói gọn một script deploy.sh
  • Ổ NVMe rút ngắn rõ hai bước nặng nhất: npm install và next build
  • Cùng máy chạy luôn được Postgres, Redis, cron, thứ serverless bắt bạn mua ngoài

Bước 1: Chuẩn bị VPS và Node

Trên VPS Ubuntu mới (2 vCPU / 2 GB trở lên cho app nhỏ, 4-6 GB nếu kèm database):

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs git
node -v   # can >= ban Node du an yeu cau

Kéo mã nguồn về và cài phụ thuộc:

git clone https://github.com/ban/du-an.git ~/app && cd ~/app
npm ci   # dung npm ci thay npm install: cai dung theo lockfile, sach va nhanh

Đây là bước đầu tiên bạn cảm nhận ổ đĩa: npm ci bung hàng vạn file nhỏ vào node_modules, trên ổ NVMe mất chừng nửa phút với dự án trung bình, trên ổ chậm có thể gấp nhiều lần.

Bước 2: Build và chạy thử

npm run build
npm run start   # mac dinh cong 3000

Mở http://IP-VPS:3000 kiểm tra nhanh (nhớ mở port tạm hoặc test bằng curl localhost:3000 ngay trên máy). Chạy được là phần khó nhất đã xong, hai bước còn lại chỉ là đóng gói cho tử tế.

Bước 3: PM2 giữ app sống 24/7

sudo npm i -g pm2
pm2 start npm --name app -- start
pm2 save && pm2 startup   # chay lenh no in ra de tu khoi dong cung VPS

Từ giờ app tự hồi sinh khi crash và tự chạy lại khi VPS reboot. Lệnh hay dùng: pm2 logs app xem log, pm2 restart app khởi động lại, pm2 monit nhìn RAM/CPU. Máy nhiều nhân muốn vắt hết CPU: pm2 start npm --name app -i max -- start chạy cluster mode.

Bước 4: Caddy - tên miền và HTTPS trong 5 dòng

Trỏ bản ghi A của tên miền về IP VPS, rồi:

sudo apt install -y caddy
# /etc/caddy/Caddyfile
ten-mien.vn {
    reverse_proxy 127.0.0.1:3000
}
sudo systemctl reload caddy

Caddy tự xin chứng chỉ HTTPS và tự gia hạn, không certbot, không cron SSL. Muốn thêm app thứ hai trên cùng VPS? Thêm một khối tên miền nữa trỏ vào port khác, xong.

Bước 5: Script deploy một lệnh

#!/bin/bash
# ~/deploy.sh: cap nhat phien ban moi
set -e
cd ~/app
git pull
npm ci
npm run build
pm2 restart app
echo "Deploy xong: $(date)"

chmod +x ~/deploy.sh: từ nay mỗi lần lên code mới chỉ cần ./deploy.sh. Nâng cấp sau này: gọi script từ GitHub Actions qua SSH là có CI/CD; hoặc để Claude Code trên chính VPS vừa sửa code vừa tự deploy, quy trình một máy khép kín.

Lỗi hay gặp và cách gỡ

  • Build chết vì thiếu RAM (thường trên gói 1-2 GB): thêm swap 2 GB hoặc build sẵn từ máy khác. Dấu hiệu: log dừng đột ngột, dmesg báo OOM.
  • Trang 502: app chưa chạy (pm2 status), hoặc Caddy trỏ sai port.
  • Đổi biến môi trường không ăn: Next.js đọc env lúc build với biến NEXT_PUBLIC_*, đổi env là phải build lại, không chỉ restart.
  • Ảnh next/image lỗi: cấu hình images.domains/remotePatterns trong next.config theo đúng nguồn ảnh.

Tệp cấu hình đầy đủ, dán là chạy

Tệp quản lý tiến trình, đặt tên ecosystem.config.js trong thư mục dự án:

module.exports = {
  apps: [{
    name: "web",
    script: "npm",
    args: "start",
    cwd: "/home/ban/app",
    instances: "max",          // dung het so nhan CPU
    exec_mode: "cluster",
    max_memory_restart: "700M",
    env: { NODE_ENV: "production", PORT: 3000 },
    error_file: "/home/ban/logs/web-err.log",
    out_file: "/home/ban/logs/web-out.log",
    time: true
  }]
}
pm2 start ecosystem.config.js
pm2 save
pm2 startup            # in ra mot lenh, chay lenh do de app song sau reboot

Cấu hình máy chủ web phía trước, tệp Caddyfile:

ten-mien.vn {
    encode zstd gzip
    reverse_proxy 127.0.0.1:3000
    header {
        Strict-Transport-Security "max-age=31536000"
        X-Content-Type-Options "nosniff"
        X-Frame-Options "SAMEORIGIN"
    }
}

Chứng chỉ bảo mật được xin và gia hạn tự động, không phải làm gì thêm. Điều kiện: tên miền đã trỏ về IP máy chủ và cổng 80, 443 đang mở.

Kịch bản cập nhật một lệnh

#!/bin/bash
# deploy.sh
set -e
cd /home/ban/app
git pull origin main
npm ci --omit=dev
npm run build
pm2 reload web --update-env     # reload thay vi restart: khong dut ket noi
pm2 save
echo "Xong luc $(date)"

Dùng reload thay restart khi chạy ở chế độ cụm: các tiến trình được thay lần lượt nên người dùng không gặp lỗi trong lúc triển khai. Với chế độ một tiến trình thì hai lệnh như nhau.

Bốn lỗi hay gặp và cách gỡ

Triệu chứngNguyên nhânCách sửa
Trang báo lỗi 502Ứng dụng chưa chạy hoặc sai cổngpm2 list xem trạng thái, curl -I localhost:3000 kiểm trực tiếp
Bị dừng giữa lúc build, thông báo hết bộ nhớMáy chủ 1 tới 2 GB không đủ cho bước buildThêm vùng nhớ tạm, hoặc build ở máy khác rồi chép kết quả sang
Ứng dụng chết sau khi đóng cửa sổ đăng nhậpChạy bằng lệnh trực tiếp thay vì qua trình quản lý tiến trìnhDùng cấu hình ở trên, và nhớ chạy pm2 save
Ảnh không hiện sau khi triển khaiThiếu thư viện xử lý ảnh, hoặc thiếu khai tên miền được phépCài thư viện xử lý ảnh, khai danh sách tên miền nguồn trong cấu hình dự án

Ba việc bảo mật làm ngay sau khi lên máy chủ

  • Đóng mọi cổng trừ 22, 80, 443. Ứng dụng chạy ở cổng 3000 chỉ nên nghe trên địa chỉ nội bộ, không phơi ra ngoài.
  • Đổi sang đăng nhập bằng khóa và tắt đăng nhập bằng mật khẩu. Máy chủ mới dựng bị dò mật khẩu trong vòng vài giờ, đây là con số thật quan sát được trong nhật ký.
  • Đưa tệp biến môi trường ra ngoài kho mã nguồn và đặt quyền đọc chỉ cho tài khoản chạy ứng dụng. Chuỗi kết nối cơ sở dữ liệu lọt lên kho mã nguồn là sự cố phổ biến bậc nhất.

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

VPS cấu hình bao nhiêu thì chạy được Next.js?

App nhỏ chưa database: 2 vCPU / 2 GB chạy tốt (build hơi chậm). Kèm Postgres + Redis + traffic thật: 4-6 vCPU / 4-6 GB là điểm êm. Chi tiết theo quy mô xem bài Next.js 15 trên VPS ở mục liên quan.

Deploy VPS có mất tính năng gì của Vercel không?

Mất phần hào nhoáng: preview URL mỗi PR, edge network toàn cầu, analytics tích hợp. Khách chủ yếu ở Việt Nam thì edge toàn cầu gần như vô nghĩa, máy đặt tại VN còn nhanh hơn. Preview thì tự dựng bằng Cloudflare Tunnel khi cần.

Standalone output có nên bật không?

Nên với người muốn gọn: output standalone trong next.config giúp bản chạy production chỉ cần thư mục .next/standalone nhẹ hơn nhiều so với vác cả node_modules, tiện khi đóng Docker hoặc chuyển máy.

Một VPS chạy được mấy site Next.js?

Tùy RAM: mỗi app production chiếm cỡ 150-400 MB lúc nhàn. Gói 6-8 GB nuôi 3-5 site vừa thoải mái, Caddy điều phối tên miền, PM2 quản danh sách tiến trình, một máy làm cả farm.

Hạ tầng NVMe Enterprise của TND
VPS ổ Enterprise U.2 NVMe, CPU xung cao, đặt tại Việt Nam
Cả bài này chạy mượt nhất trên nền ổ nhanh: VPS NVMe của TND, npm ci và next build ăn IOPS thì đã có Enterprise U.2 NVMe RAID 10, PM2 + Caddy chạy nền trên Xeon Platinum xung cao. Bàn giao 5 phút, đặt tại Việt Nam, khách Việt của bạn vào trang nhanh hơn hẳn hosting ngoại.
Dùng thử VPS NVMe, bàn giao trong 5 phút

Bài viết liên quan