Skip to main content

Self-hosted Next.js — deploy on your own server

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.