Deploy a Next.js App with Nginx and PM2
Run a Node app as a persistent background process with PM2, then reverse-proxy it through Nginx for TLS and a real domain.
By DevStudio Online Team · Published September 3, 2026
A Next.js app running in server mode (not a static export) needs a persistent Node process — one that survives an SSH disconnect and restarts itself after a crash or a server reboot. PM2 handles that; Nginx sits in front of it for TLS and a real domain name. Neither replaces the other.
Install and run under PM2
npm install -g pm2
cd /var/www/my-nextjs-app
npm ci
npm run build
pm2 start npm --name "my-app" -- startpm2 start npm --name "my-app" -- start runs npm start (which runs next start under the hood) as a managed process named my-app — the name is what you'll use for every later pm2 command, so make it something you'll recognize in pm2 list six months from now.
Make PM2 survive a reboot
pm2 save # snapshot the currently running process list
pm2 startup # prints a systemd command — copy-paste and run it, it needs sudoSkipping pm2 startup is the most common reason a working deployment "randomly" goes down after a server reboot — PM2 itself doesn't survive a reboot unless you've registered it as a systemd service.
The Nginx reverse proxy
Next.js typically listens on 127.0.0.1:3000 by default. Nginx's job is to sit on ports 80/443 (the ones the internet actually reaches) and forward to that internal port:
server {
listen 80;
server_name myapp.example.com;
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;
}
}The four proxy_set_header lines matter more than they look — without them, your Next.js app sees every request as coming from 127.0.0.1 (Nginx itself), not the real visitor's IP or the actual Host header they used, which breaks anything that logs IPs, rate-limits, or generates absolute URLs from the request.
Add TLS
sudo certbot --nginx -d myapp.example.comCertbot rewrites the server block to add a matching listen 443 ssl block and an HTTP→HTTPS redirect automatically — there's rarely a reason to hand-write the SSL directives yourself.
Redeploying without downtime
git pull
npm ci
npm run build
pm2 restart my-apppm2 restart is near-instant — PM2 keeps the old process serving traffic until the new one is ready, rather than tearing it down first and leaving a gap. Confirm the new build actually made it in with pm2 logs my-app --lines 20 right after restarting.