Deploy Next.js lên VPS, không cần Vercel

Cách deploy Next.js lên VPS: output standalone, systemd, nginx cache /_next/static, SSL miễn phí, cập nhật không gián đoạn và những gì bạn mất so với Vercel.

Sơ đồ: server Next.js standalone chạy sau nginx có SSL trên VPS, so sánh với Vercel

Ý chính

  • Bật output standalone, chép public và .next/static vào .next/standalone, rồi chạy node server.js với PORT và HOSTNAME.
  • Trên một server, tối ưu ảnh, ISR và Proxy (trước gọi là middleware) chạy được ngay; chạy nhiều instance thì cần cache handler dùng chung và khóa Server Functions cố định.
  • Để nginx tự phục vụ /_next/static với cache immutable một năm, và giữ file tĩnh cũ sau mỗi lần deploy để tránh version skew.
  • Vercel Pro có giá 20 USD mỗi developer seat mỗi tháng tại thời điểm viết; VPS rẻ hơn nhưng bạn phải tự lo CDN, preview và vận hành.

Để deploy Next.js lên VPS mà không cần Vercel, bạn đặt output: 'standalone' trong next.config, chạy next build, chép hai thư mục public và .next/static vào .next/standalone, rồi chạy node server.js bằng systemd với biến PORT và HOSTNAME. Đặt nginx phía trước để lo tên miền, SSL và cache dài hạn cho /_next/static. Trên một server duy nhất, tối ưu ảnh, ISR và Proxy (trước đây là middleware) đều chạy ngay; thứ bạn mất là CDN toàn cầu, preview deployment của Vercel và việc có người khác vận hành hạ tầng cho bạn.

Bài này chỉ nói những gì riêng của Next.js. Phần cài đặt chung (Node LTS, user riêng, tường lửa, systemd hay PM2) có trong bài deploy Node.js lên VPS.

Rời Vercel thì bạn mất những gì?

Ít hơn nhiều người nghĩ, miễn là bạn chạy một server. Hướng dẫn self-host của Next.js nêu rõ tính năng nào chạy được trên một server Node.js thông thường:

Tính năng Trên một VPS (next start hoặc standalone) Khác biệt
Tối ưu ảnh (next/image) Chạy được, không cần cấu hình Từ Next.js 15, sharp được dùng tự động với next start và output standalone. Việc resize ảnh tốn CPU và RAM của VPS.
ISR và data cache Chạy được; cache lưu trên ổ đĩa local Nhiều instance thì mỗi cái giữ cache riêng, trừ khi bạn thêm cache handler dùng chung, ví dụ Redis
Proxy (trước là middleware) Chạy được Next.js 16 đổi tên middleware thành Proxy, mặc định chạy trên runtime Node.js. Nó chạy trên server của bạn, không phải ở edge.
Streaming và Suspense Chạy được nginx không được buffer response (xem bên dưới)
Preview deployment Không có Vercel tạo preview cho mỗi lần push lên nhánh không phải production và mỗi pull request. Trên VPS bạn phải tự dựng hoặc dùng một site staging.
CDN toàn cầu Không có File tĩnh được phục vụ từ một nơi, trừ khi bạn thêm CDN như Cloudflare
Vận hành Bạn tự lo Cập nhật hệ điều hành, backup, giám sát và tài nguyên

Cái bẫy thật sự là chạy nhiều hơn một instance. Tài liệu Next.js liệt kê những thứ khi đó bắt buộc phải có: cache handler dùng chung, cùng một NEXT_SERVER_ACTIONS_ENCRYPTION_KEY trên mọi instance (nếu không sẽ gặp lỗi "Failed to find Server Action"), và deploymentId để xử lý version skew. Nếu một VPS là đủ, bạn bỏ qua được hết.

Vercel hay VPS: chi phí bao nhiêu?

Tại thời điểm viết (tháng 10/2026), trang giá của Vercel ghi:

Vercel Hobby Vercel Pro VPS Hetzner CX23
Giá 0 USD 20 USD mỗi developer seat mỗi tháng 5,99 EUR/tháng chưa VAT
Mục đích sử dụng Cá nhân, phi thương mại Dự án thương mại Tùy ý
Bao gồm Giới hạn cố định, không mua thêm được 20 USD credit, 1 TB Fast Data Transfer, 10 triệu edge request mỗi tháng 2 vCPU, 4 GB RAM, 40 GB NVMe, 20 TB băng thông
Vượt mức Không được Băng thông từ 0,15 USD/GB Băng thông thêm tính theo TB

Số liệu Hetzner lấy từ trang gói Cost-Optimized (khu vực EU). Hai bên không hoàn toàn tương đương: giá Vercel đã gồm CDN, preview, hạ tầng build và không có server nào phải bảo trì, còn VPS cho bạn hóa đơn cố định, nhiều băng thông hơn và chỗ cho cả database lẫn các website khác. Nếu team coi trọng preview và không muốn vận hành gì, Vercel vẫn đáng tiền.

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

Các bước dưới đây giả định đã có Node.js 22 hoặc 24, nginx và user nextapp.

Bước 1: Bật output standalone

// next.config.js
module.exports = {
  output: 'standalone',
}

Khi next build, Next.js dò xem mỗi route cần file nào và ghi ra một thư mục tự chạy được ở .next/standalone, gồm phần node_modules cần thiết và một file server.js tối giản. Bạn deploy thư mục đó mà không cần npm install trên server. Tài liệu output lưu ý server.js không tự chép public và .next/static, vì hai thư mục này "nên được CDN phục vụ". Bạn tự chép:

npm ci
npm run build
cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/

Bước 2: Để ý RAM khi build

next build thường tốn RAM hơn app lúc chạy, và một bản build bị OOM killer dừng chỉ in ra Killed. Hướng dẫn tối ưu bộ nhớ của Next.js đưa ra các cách: experimental.webpackMemoryOptimizations, tắt source map (productionBrowserSourceMaps: false), và kiểm tra kiểu trong CI thay vì lúc build. Trên VPS nhỏ, bạn có thể:

  • Thêm swap, để lúc cao điểm build chậm lại thay vì bị dừng.
  • Build trong CI (GitHub Actions) rồi chỉ gửi .next/standalone lên server. Standalone giúp việc này dễ vì thư mục không cần cài node_modules.

Bước 3: Chạy server.js bằng systemd

Tạo /etc/systemd/system/nextapp.service:

[Unit]
Description=Next.js app
After=network.target

[Service]
User=nextapp
Group=nextapp
WorkingDirectory=/srv/nextapp/current
Environment=NODE_ENV=production
Environment=PORT=3000
Environment=HOSTNAME=127.0.0.1
EnvironmentFile=-/etc/nextapp.env
ExecStart=/usr/local/bin/node server.js
Restart=on-failure
KillSignal=SIGTERM
TimeoutStopSec=30
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/srv/nextapp

[Install]
WantedBy=multi-user.target

/srv/nextapp/current là symlink trỏ tới thư mục release chứa server.js (nội dung của .next/standalone). HOSTNAME=127.0.0.1 giữ server không lộ ra interface công khai; tài liệu output xác nhận server.js đọc PORT và HOSTNAME. ReadWritePaths quan trọng vì ISR ghi cache xuống đĩa bên trong .next. Thời gian dừng 30 giây theo đúng hướng dẫn self-host: gửi SIGTERM và chờ 10 đến 30 giây để các request đang chạy và callback after() hoàn tất.

Secret chỉ dùng lúc chạy để trong /etc/nextapp.env. Nhớ rằng biến NEXT_PUBLIC_* được nhúng lúc build, nên đổi trên server sẽ không có tác dụng cho tới khi bạn build lại.

sudo systemctl daemon-reload
sudo systemctl enable --now nextapp
curl -I http://127.0.0.1:3000

Bước 4: nginx với cache cho /_next/static

File trong /_next/static có mã hash trong tên, và Next.js đã gửi sẵn Cache-Control: public, max-age=31536000, immutable cho chúng. Hãy để nginx phục vụ thẳng từ đĩa để Node không phải xử lý các request này. Biến $connection_upgrade cho WebSocket là map trong bài Node.js:

upstream nextapp {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name example.com www.example.com;

    location /_next/static/ {
        alias /srv/nextapp/static/;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    location / {
        proxy_pass http://nextapp;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        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;
    }
}

/srv/nextapp/static/ là thư mục dùng chung, mỗi lần deploy chép thêm file .next/static mới vào (cp -rn) và giữ file cũ một thời gian. Cách này tránh một lỗi kinh điển khi self-host: người dùng mở trang trước lúc deploy, sau đó tải một chunk cũ không còn tồn tại. Tài liệu Next.js gọi đây là version skew và ghi rằng giữ lại các asset immutable cũ sẽ tránh được nó.

Để streaming và Suspense hoạt động, nginx không được buffer. Bạn thêm proxy_buffering off; vào khối location /, hoặc gửi header X-Accel-Buffering: no từ headers() trong next.config.js như hướng dẫn self-host.

Bước 5: SSL bằng Certbot

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

Certbot lấy chứng chỉ Let's Encrypt miễn phí, thêm khối server HTTPS và cài timer tự gia hạn. Nếu muốn có CDN cho người dùng ở xa server, xem bài dùng Cloudflare với VPS; header immutable ở trên giúp Cloudflare cache /_next/static an toàn.

Bước 6: Cập nhật không gián đoạn

systemctl restart làm rớt request vài giây trong lúc server mới khởi động. Chuyển kiểu blue/green tránh được điều đó:

  1. Build vào thư mục mới, ví dụ /srv/nextapp/releases/20261021-1200, rồi chép file tĩnh vào thư mục static dùng chung.
  2. Khởi động bản mới ở cổng đang rảnh bằng systemd template unit (nextapp@3001.service, dùng Environment=PORT=%i).
  3. Chờ tới khi curl -fsS http://127.0.0.1:3001/ trả lời.
  4. Đổi dòng server trong khối upstream sang cổng 3001, rồi nginx -t && systemctl reload nginx.
  5. Dừng instance cũ khi các request đang chạy đã xong (systemctl stop nextapp@3000).

Lưu ý các trang ISR sẽ tạo lại từ cache rỗng trong thư mục release mới, và hãy giữ release trước đó để rollback chỉ là lặp lại bước 2 đến 5 với nó. Bài tự động deploy từ GitHub hướng dẫn kích hoạt việc này mỗi lần push.

Deploy Next.js bằng ZoPanel

ZoPanel chạy app như một service sau nginx, có SSL và chỉ chuyển release sau khi qua health check, bên trong một hosting account có giới hạn tài nguyên riêng:

  1. Tạo website loại Git deploy với repository và nhánh; giữ Free SSL (Let's Encrypt) bật. ZoPanel nhận diện Next.js từ package.json và đề xuất script start của bạn, hoặc npx next start. Tối ưu ảnh chạy được vì next start tự dùng sharp.
  2. ZoPanel đặt sẵn PORT và HOST=127.0.0.1, và next start tự đọc PORT. Đừng dùng output: 'standalone' chung với next start. Nếu muốn dùng standalone, tắt Auto-detect trong Build & run, nối hai lệnh cp vào build command bằng &&, và đặt start command là node .next/standalone/server.js. server.js của standalone đọc HOSTNAME chứ không phải HOST, nên thêm HOSTNAME=127.0.0.1 trong Environment variables.
  3. Mỗi lần deploy build vào một thư mục release mới, trong giới hạn CPU và RAM của gói. Bản mới chạy ở slot đang rảnh và phải trả lời Health check path với mã dưới 500 trong 90 giây thì nginx mới chuyển sang; nếu không, bản đang chạy vẫn giữ nguyên. Rollback giữ 5 bản build gần nhất.
  4. Bật Deploy when main is pushed và thêm webhook vào GitHub, GitLab hoặc Gitea.

Cần biết: NODE_ENV=production chỉ được đặt lúc chạy nên devDependencies vẫn được cài để build; biến NEXT_PUBLIC_* thêm trong Environment variables có sẵn lúc build; và file app ghi bên trong thư mục release (kể cả cache ISR) sẽ bắt đầu lại từ đầu ở mỗi release, nên file upload phải để trong Persistent paths. Nếu build bị dừng, hãy tăng RAM của gói. Use every CPU core chạy một tiến trình cho mỗi CPU, tức là app trở thành nhiều instance: chỉ bật khi bạn đã xử lý cache dùng chung và state trong bộ nhớ như ở trên. ZoPanel không có preview deployment hay CDN toàn cầu. Chi tiết trong Node.js apps và Git deploy.

Kết luận

Self-host Next.js trên một VPS là bài toán đã có lời giải: output standalone (hoặc next start), systemd, nginx phục vụ /_next/static với cache immutable, Certbot và chuyển release kiểu blue/green. Khi cần nhiều instance hoặc preview, đó là lúc Vercel xứng với giá tiền. Nếu muốn build, health check, rollback và SSL được làm sẵn, hãy thử gói Free của ZoPanel (3 hosting account, 10 website, 10 database, không giới hạn thời gian) trên một VPS thử nghiệm và deploy repository Next.js của bạn.

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

Có dùng được gói Hobby miễn phí của Vercel cho website thương mại không?

Không. Trang giá của Vercel ghi gói Hobby dành "cho mục đích cá nhân, phi thương mại" với giới hạn sử dụng cố định. Dự án thương mại cần gói Pro, tại thời điểm viết là 20 USD mỗi developer seat mỗi tháng, hoặc tự host.

next/image có chạy trên VPS không?

Có. Từ Next.js 15, sharp được dùng tự động với next start và output standalone, nên ảnh được resize ngay trên server của bạn. Trên Linux dùng glibc, tài liệu Next.js khuyên xem phần cấu hình memory allocator của sharp nếu tối ưu ảnh tốn quá nhiều RAM. Bạn cũng có thể đặt unoptimized và tự tối ưu ảnh.

Build Next.js trên VPS cần bao nhiêu RAM?

Tùy kích thước dự án, nên hãy đo: chạy next build --experimental-debug-memory-usage hoặc theo dõi RAM trong lúc build. Trên VPS nhỏ, thêm swap hoặc build trong CI rồi deploy thư mục standalone. Server lúc chạy thường cần ít RAM hơn nhiều so với lúc build.

Self-host Next.js có cần Docker không?

Không. node server.js chạy bằng systemd sau nginx đơn giản hơn trên một VPS. Docker hữu ích khi bạn muốn cùng một image trên nhiều server hoặc trong orchestrator, và tài liệu deploy của Next.js có hướng dẫn cho trường hợp đó.

ISR có chạy khi self-host không?

Có. Với một instance, ISR và revalidatePath/revalidateTag dùng cache trên filesystem local mà không cần cấu hình. Với nhiều instance, bạn cần cache handler dùng chung, ví dụ Redis, để revalidate ở một instance có tác dụng trên các instance còn lại.