TL;DR
Complete guide to deploying a production Next.js application on Ubuntu 24.04 with standalone output, PM2, nginx, and SSL.
Key facts
- Topology
- Next.js on Ubuntu 24.04
TL;DR
This guide covers deploying a self-hosted Next.js application on Ubuntu 24.04 using the standalone output mode, PM2 for process management, nginx for reverse proxy and static asset serving, and Certbot for SSL — a production-ready setup without Vercel.
Prerequisites
- Ubuntu 24.04 LTS with root or sudo access (minimum 2 GB RAM for builds)
- A domain name with DNS pointing to your server
- Your Next.js project in a Git repository
Step 1 — System preparation and Node.js 20 LTS
Update the system and install Node.js 20 LTS:
sudo apt update && sudo apt upgrade -y
sudo apt install -y curl git build-essential
Install Node.js via NodeSource:
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v && npm -v
Create a deploy user:
sudo adduser deploy
sudo usermod -aG sudo deploy
Step 2 — Configure standalone output
In your next.config.js, enable standalone output mode. This creates a self-contained deployment bundle that includes only the dependencies your application needs:
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
module.exports = nextConfig;
Step 3 — Deploy and build
sudo mkdir -p /var/www/nextapp
sudo chown deploy:deploy /var/www/nextapp
cd /var/www/nextapp
git clone git@github.com:yourorg/nextapp.git .
npm ci
Create your .env.production file with the required environment variables, then build:
cp .env.example .env.production
npm run build
Copy the static and public directories into the standalone output:
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static
Step 4 — PM2 process management
Install PM2 and configure an ecosystem file:
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',
node_args: '--max-old-space-size=1536',
env: {
NODE_ENV: 'production',
PORT: 3000,
HOSTNAME: '0.0.0.0',
},
wait_ready: true,
listen_timeout: 10000,
kill_timeout: 5000,
}],
};
Start and persist:
pm2 start ecosystem.config.js
pm2 startup systemd
pm2 save
Verify the app is running:
curl -I http://127.0.0.1:3000
Step 5 — nginx reverse proxy with static asset caching
sudo apt install -y nginx
Create /etc/nginx/sites-available/nextapp:
upstream nextjs_upstream {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name example.com;
gzip on;
gzip_types text/plain application/json application/javascript text/css image/svg+xml;
gzip_min_length 1000;
location /_next/static {
alias /var/www/nextapp/.next/static;
expires 365d;
access_log off;
add_header Cache-Control "public, immutable";
}
location /public {
alias /var/www/nextapp/public;
expires 30d;
access_log off;
}
location / {
proxy_pass http://nextjs_upstream;
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_read_timeout 120s;
}
}
Enable the site:
sudo ln -s /etc/nginx/sites-available/nextapp /etc/nginx/sites-enabled/
sudo rm -f /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx
Step 6 — SSL via Certbot and firewall
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com
sudo ufw allow 'Nginx Full'
sudo ufw allow OpenSSH
sudo ufw enable
Step 7 — Health check and environment variables
Add a health check API route in your Next.js app at pages/api/health.ts:
export default function handler(req, res) {
res.status(200).json({ status: 'ok', uptime: process.uptime() });
}
Verify from the server:
curl http://127.0.0.1:3000/api/health
With Reflex
Once deployed, connect Reflex for continuous monitoring of your Next.js stack. Reflex tracks PM2 process health, nginx error rates, SSL certificate expiry, memory usage, and disk thresholds. It orchestrates zero-downtime deploys with health-gated rollouts, and can auto-recover from crashes and memory spikes — replacing manual SSH maintenance with audited, repeatable playbooks. See How it works.