TL;DR
Complete guide to self-hosting a Next.js application with SSR on your own Linux server using PM2 and nginx.
Key facts
- Topology
- Next.js SSR on Linux
TL;DR
Self-hosting Next.js gives you full control over your SSR infrastructure without Vercel vendor lock-in. This guide covers building for production, running under PM2, configuring nginx for static assets and reverse proxying, and handling ISR on self-hosted infrastructure.
Prerequisites
- Ubuntu 24.04 or any modern Linux distribution
- Node.js 22 LTS
- A domain name with DNS pointing to your server
- At least 2 GB RAM (Next.js builds are memory-intensive)
Step 1 — Configure standalone output
Update next.config.js for self-hosted deployment:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
compress: false,
};
module.exports = nextConfig;
Setting output: 'standalone' produces a self-contained directory with only the files needed for production. Disabling compress lets nginx handle gzip more efficiently.
Step 2 — Build the application
cd /var/www/nextapp
git clone git@github.com:yourorg/nextapp.git .
npm ci
Create .env.production with your runtime variables. Only NEXT_PUBLIC_* variables are inlined at build time — server-side variables are read at runtime:
cp .env.example .env.production
# Set NEXT_PUBLIC_API_URL, DATABASE_URL, etc.
npm run build
Copy the static and public directories into the standalone output:
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
Step 3 — PM2 process management
sudo npm install -g pm2
Create ecosystem.config.js:
module.exports = {
apps: [{
name: 'nextapp',
script: '.next/standalone/server.js',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '1G',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '0.0.0.0',
},
error_file: '/var/log/pm2/nextapp-error.log',
out_file: '/var/log/pm2/nextapp-out.log',
merge_logs: true,
}],
};
The standalone server respects the PORT and HOSTNAME environment variables. Setting HOSTNAME to 0.0.0.0 ensures the server binds to all interfaces — required when nginx proxies to 127.0.0.1.
sudo mkdir -p /var/log/pm2
pm2 start ecosystem.config.js --env production
pm2 startup systemd
pm2 save
Step 4 — nginx reverse proxy with static asset caching
sudo apt install -y nginx
Create /etc/nginx/sites-available/nextapp:
server {
listen 80;
server_name example.com;
location /_next/static {
alias /var/www/nextapp/.next/standalone/.next/static;
expires 365d;
add_header Cache-Control "public, immutable";
access_log off;
}
location /public {
alias /var/www/nextapp/.next/standalone/public;
expires 30d;
access_log off;
}
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 'upgrade';
proxy_cache_bypass $http_upgrade;
}
}
The Upgrade and Connection headers are included for WebSocket support. In production SSR, WebSockets are used by features like Next.js Server Actions streaming — HMR is development-only and won't be active.
sudo ln -s /etc/nginx/sites-available/nextapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Step 5 — SSL
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
ISR on self-hosted
Incremental Static Regeneration works on self-hosted Next.js, but the cache lives on the local filesystem (.next/cache). In a multi-instance PM2 cluster, each worker maintains its own ISR cache. For consistency, either run a single instance handling ISR routes or use a shared filesystem mount. If ISR cache staleness is a concern, consider revalidateTag with on-demand revalidation triggered from your CMS or API.
Monitoring
Monitor the Next.js processes through PM2:
pm2 monit
pm2 logs nextapp --lines 100
With Reflex
Reflex provides unified monitoring for self-hosted Next.js, tracking PM2 cluster health, SSR response times, static asset cache-hit rates via nginx, and memory consumption during builds. It orchestrates zero-downtime reloads with automatic rollback if the health check fails after deploy. See How it works.