How to Deploy Next.js SSR Web Applications on Linux VPS with PM2 & Standalone Output

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:

  1. The entire contents of .next/standalone/
  2. The public/ folder (copied into /var/www/my-nextjs-app/public/)
  3. 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.

Discover CpanelFree Developer VPS →

Leave a Comment