ShortIQ

ShortIQ

Deployment

How to Deploy Next.js on Ubuntu 24 with Nginx and PM2

A complete guide to self-hosting a Next.js application on Ubuntu 24.04 with Nginx as a reverse proxy, PM2 for process management, and a free SSL certificate from Certbot.

June 11, 2026ShortIQ Editorial Team

Prerequisites

This guide walks through deploying a Next.js application on Ubuntu 24.04 LTS without relying on Vercel, Netlify, or any managed hosting platform. By the end, your application will run at https://yourdomain.com, managed by PM2 in cluster mode for zero-downtime restarts, proxied through Nginx, and secured with a free SSL certificate from Certbot.

You need an Ubuntu 24.04 server (any VPS provider), a domain with an A record pointing to your server IP, SSH access as a non-root user with sudo, and a Next.js application with a build script in package.json. The application must be configured for standalone output or standard server mode — this guide covers both.

Step 1: Install Node.js with NVM

Use NVM (Node Version Manager) to install Node.js rather than apt. NVM lets you switch Node versions without touching the system Node installation and avoids permission issues with global npm packages. Install NVM with the official install script, then install and use Node.js 20 LTS.

bash
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
nvm alias default 20
node --version && npm --version

Step 2: Install PM2 and Clone Your Application

PM2 is a production process manager for Node.js. Install it globally, then clone your Next.js application and install dependencies. PM2 will manage starting, stopping, and restarting the application.

bash
npm install -g pm2
sudo mkdir -p /var/www/myapp
sudo chown $USER:$USER /var/www/myapp
cd /var/www/myapp
git clone https://github.com/yourusername/yourrepo.git .
npm ci
npm run build

Step 3: Configure next.config.js for Standalone Output

For production deployments, enable the standalone output mode in next.config.js. This creates a self-contained server in .next/standalone that includes only the files needed to run, significantly reducing the deployment artifact size.

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone'
};

module.exports = nextConfig;

Step 4: Create a PM2 Ecosystem File

Create an ecosystem.config.js file in your application directory. This file configures PM2 to run your Next.js application in cluster mode with one worker per CPU core, which maximises throughput on multi-core servers and enables zero-downtime reloads.

javascript
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'myapp',
    script: '.next/standalone/server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      HOSTNAME: '127.0.0.1'
    },
    max_memory_restart: '512M',
    error_file: '/var/log/myapp/error.log',
    out_file: '/var/log/myapp/out.log'
  }]
};
bash
sudo mkdir -p /var/log/myapp
sudo chown $USER:$USER /var/log/myapp
# Copy static assets required by standalone mode
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
pm2 start ecosystem.config.js
pm2 save
pm2 startup  # follow the output command to enable autostart

Step 5: Configure Nginx as a Reverse Proxy

Install Nginx and create a site configuration that proxies all requests to the PM2-managed Next.js server on port 3000. The application binds to 127.0.0.1 so only Nginx can reach it.

bash
sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/myapp
nginx
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;

    # Cache static assets
    location /_next/static/ {
        alias /var/www/myapp/.next/static/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://127.0.0.1:3000;
        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_read_timeout 60s;
    }
}
bash
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

Step 6: Add SSL with Certbot

Install Certbot and obtain a free SSL certificate. Certbot updates the Nginx config automatically and installs a renewal timer. Test the renewal to confirm it is working.

bash
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo certbot renew --dry-run
sudo ufw allow 'Nginx Full'
sudo ufw enable

Deploying Updates with Zero Downtime

PM2 cluster mode enables zero-downtime reloads: pm2 reload restarts workers one at a time, keeping the others serving traffic during the reload. This means deploys have no observable downtime even under load.

bash
cd /var/www/myapp
git pull origin main
npm ci
npm run build
# Copy fresh static assets for standalone mode
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
# Zero-downtime reload
pm2 reload myapp
pm2 status

Monitoring and Troubleshooting

PM2 provides built-in process monitoring. Use pm2 status to see running processes and their CPU and memory usage, pm2 logs to tail application output, and pm2 monit for a real-time dashboard. For production, consider connecting PM2 to PM2 Plus for remote monitoring and alerts.

Common issues: if Nginx returns 502 Bad Gateway, check that PM2 is running with pm2 status and that the app is listening on port 3000 with ss -tlnp | grep 3000. If the app fails to start, check the error logs with pm2 logs --err. If static assets are missing after a deploy, verify that the static copy commands in the deploy script ran correctly.

FAQ

Should I self-host Next.js or use Vercel?

Self-hosting makes sense when you have existing server infrastructure, want to avoid per-seat pricing at scale, or need to run the frontend close to a self-hosted backend on the same server. Vercel is the better default for new projects: it handles CDN, scaling, preview deployments, and zero-downtime deploys automatically. The operational overhead of self-hosting is worth it at scale but adds real work for a small team.

Does this setup support React Server Components and Server Actions?

Yes. The standalone server mode runs the full Next.js server including React Server Components, Server Actions, API routes, and middleware. All Next.js features that require a Node.js server work with this setup. The only Next.js features that do not work self-hosted are Vercel-specific services like Vercel KV, Blob, Edge Config, and Vercel Analytics.

How do I scale this setup to multiple servers?

PM2 cluster mode uses all CPU cores on a single server. To scale across multiple servers, put a load balancer in front of two or more servers running this same setup. You will need sticky sessions or a shared session store if your app uses server-side sessions, and a shared cache layer if you use ISR or cache API responses. For most applications, a single well-provisioned server with PM2 cluster mode handles significant traffic before requiring multiple nodes.

How do I handle environment variables in production?

Set NEXT_PUBLIC_ variables at build time — they are baked into the static output. Set server-side variables in the PM2 ecosystem.config.js env section or in a .env.production file that is read at startup. Never commit secrets to your git repository. Use a secrets manager or CI/CD environment variable injection for sensitive values like database URLs and API keys.

Related free tools

If you want to turn this topic into action, use one of ShortIQ's free tools for campaign planning, UTM structure, or QR distribution.

Continue Reading

Explore more guides on link shortener SaaS strategy, Bitly alternatives, and white label link management.

Free newsletter

Get new guides in your inbox

We publish practical guides on dev tooling, prompt engineering, marketing workflows, and deployment. No fluff — straight to the point.

No spam. Unsubscribe any time.

Was this article helpful?

Tell us if this guide solved the problem or what was still missing. We use this to improve the blog and only follow up if you explicitly allow it.

We use this to improve tutorials, examples, and technical depth.