Next.js is the dominant React framework for building modern, search-engine-optimized web applications. However, hosting Next.js applications on commercial serverless platforms like Vercel often results in exorbitant monthly invoices as soon as traffic scales. High bandwidth egress markups, arbitrary serverless execution function timeouts, and costly concurrency seat limits penalize growing applications.
By leveraging Next.js’s built-in standalone build output, you can package an entire Next.js SSR application into an ultra-lean Node.js bundle that runs on an affordable Linux VPS. Paired with PM2 process manager and an Nginx reverse proxy, self-hosting Next.js delivers continuous sub-20ms SSR response times, eliminates cold-start latency, and reduces infrastructure costs by up to 90%.
1. Enabling Standalone Build Output in next.config.js
By default, a Next.js build requires your entire node_modules folder—often weighing several gigabytes. The standalone feature traces your project’s import tree and compiles only the strict dependencies required for production into a lightweight .next/standalone folder.
Open next.config.js (or next.config.mjs) and enable standalone mode:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
poweredByHeader: false,
reactStrictMode: true,
};
module.exports = nextConfig;
Execute the production build on your local machine or CI/CD runner:
npm run build
Next.js creates an optimized self-contained web server at .next/standalone/server.js weighing less than 80MB.
2. Preparing the Production Linux VPS & Node.js Runtime
Log in to your VPS as a non-root administrative user and install Node.js 20 LTS alongside PM2:
# Install NodeSource repository and Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs nginx
# Install PM2 process manager globally
sudo npm install -g pm2
Create the target application deployment directory:
sudo mkdir -p /var/www/my-nextjs-app
sudo chown -R $USER:$USER /var/www/my-nextjs-app
3. Transferring Artifacts & Managing Static Assets
To run a standalone Next.js build, you must deploy three specific folders to /var/www/my-nextjs-app:
- The entire contents of
.next/standalone/ - The
public/folder (copied into/var/www/my-nextjs-app/public/) - The compiled static assets from
.next/static/(copied into/var/www/my-nextjs-app/.next/static/)
Sync these folders efficiently via rsync:
# Run from your local repository
rsync -avz --delete .next/standalone/ deployer@your-vps-ip:/var/www/my-nextjs-app/
rsync -avz --delete public/ deployer@your-vps-ip:/var/www/my-nextjs-app/public/
rsync -avz --delete .next/static/ deployer@your-vps-ip:/var/www/my-nextjs-app/.next/static/
4. Configuring PM2 Process Manager Ecosystem
Create an ecosystem.config.js file in /var/www/my-nextjs-app/ to manage multi-core clustering and automatic restarts:
module.exports = {
apps: [
{
name: 'nextjs-production',
script: 'server.js',
cwd: '/var/www/my-nextjs-app',
instances: 'max', // Scales across all available CPU cores in cluster mode
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '127.0.0.1',
},
max_memory_restart: '512M',
listen_timeout: 10000,
kill_timeout: 5000,
},
],
};
Start the application cluster and save the systemd boot startup hook:
cd /var/www/my-nextjs-app
pm2 start ecosystem.config.js
pm2 save
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u $USER --hp /home/$USER
5. Nginx Reverse Proxy & Static Asset Caching
Configure Nginx to terminate SSL and serve immutable static files directly from disk without invoking Node.js worker cycles:
server {
listen 80;
listen 443 ssl http2;
server_name yourdomain.com;
ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
# Serve Next.js static chunks directly from disk with 1-year immutable caching
location /_next/static/ {
alias /var/www/my-nextjs-app/.next/static/;
expires 365d;
access_log off;
add_header Cache-Control "public, max-age=31536000, immutable";
}
# Serve public static assets directly
location /public/ {
alias /var/www/my-nextjs-app/public/;
expires 30d;
access_log off;
}
# Proxy dynamic SSR requests to PM2 Node.js cluster
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header 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;
proxy_cache_bypass $http_upgrade;
}
}
5. Zero-Downtime Rolling Deployments with PM2 Reload & Git Hooks
Deploying application updates to production without interrupting active user sessions is critical for modern Next.js deployments. Instead of executing pm2 restart, which terminates the existing process before starting the new one, configure graceful zero-downtime reloads:
- Graceful Cluster Reloading: PM2 supports
pm2 reload nextjs-app. When invoked, PM2 starts the new worker process, waits for it to listen on the local port, and only then issues a SIGINT signal to the old process. This guarantees that pending HTTP requests finish executing cleanly. - Automated Post-Receive Git Hook: Configure an automated deployment pipeline directly on your Linux VPS using Git hooks:
#!/bin/bash # /var/repo/nextjs.git/hooks/post-receive TARGET="/var/www/nextjs-app" GIT_DIR="/var/repo/nextjs.git" BRANCH="main" while read oldrev newrev ref do if [[ $ref =~ .*/$BRANCH$ ]]; then echo "Master ref received. Deploying Next.js to production..." git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f cd $TARGET npm ci npm run build cp -r public .next/standalone/ cp -r .next/static .next/standalone/.next/ pm2 reload nextjs-app echo "Deployment complete with zero downtime!" fi done - Handling Node.js Memory Leaks: By setting
max_memory_restart: '1G'in your PM2 ecosystem configuration, PM2 automatically recycles worker processes that exceed the threshold without downtime.
6. Edge ISR (Incremental Static Regeneration) Caching with Nginx Microcaching
Combine Next.js built-in ISR with Nginx proxy microcaching for extreme scale. By caching static HTML fragments in shared Nginx memory zones (proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=NEXT_CACHE:100m inactive=60m), your VPS can effortlessly absorb viral traffic spikes of tens of thousands of requests per second without touching Node.js runtime threads.
Deploy Next.js on CpanelFree High-Speed VPS
Say goodbye to unpredictable serverless billing. Run your Next.js applications on dedicated CPU cores, unmetered bandwidth, and lightning-fast NVMe storage on CpanelFree.
