
How to Deploy a Next.js App on a VPS the Production Way
Mahmud Hasan
October 20, 2026
Most guides teach you to deploy a Next.js app on a VPS in five commands. The commands work — what's missing are the five rules you only learn when the same deploy has to survive a year of production: the build verification nobody does, the env var that demands a rebuild, the restart that must happen last. Here are both.
Step 1 — Get the right Node on the box
Next.js 16 needs Node 20.9 or newer, and Node 22 LTS is the sane default. Ubuntu's apt repositories ship an ancient Node that will either refuse to run your build or fail in weird ways halfway through it, so install from NodeSource instead:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v # should print v22.x
If the VPS carries global NODE_ENV=production — some managed boxes do — plain npm install will quietly skip your devDependencies, and the build dies with a missing-package error that looks nothing like the real cause. Run npm install --include=dev in that case, or just use npm ci, which is what you want on a server anyway: it installs exactly what the lockfile says, so the box matches your laptop.
Step 2 — The build is where VPS deploys go wrong
This is where most guides get vague and most deploys go wrong. The sequence is:
git pull
npm ci
npm run build
Then wait for the build to exit and confirm .next/BUILD_ID exists before touching anything else. If you restart the process manager while the build is still writing, you get a half-written .next directory — the app starts, serves your old version, and you spend an hour convinced the deploy "didn't take." Check the exit code first. It's boring advice and it has saved more deploys than any other single habit.
Two more build truths nobody puts in the quickstart:
First, anything prefixed NEXT_PUBLIC_ is inlined into the JavaScript bundle at build time. It is not read at runtime. If you change a NEXT_PUBLIC_API_URL, you must rebuild — a restart won't pick it up. (Server-only variables, no prefix, only need a restart.) Mixing these up is the most common cause of "my new key isn't working."
Second, next build is memory-hungry. On a 1 GB box it can get OOM-killed mid-build, leaving a stale .next that serves yesterday's site. Either build on a box with 2 GB or more, or add a 2 GB swap file before building. A killed build's worst symptom is silence — the old version keeps running, so nobody notices.
Step 3 — Keep it alive with PM2
Running npm run start in your SSH session works until the session closes, which is approximately four minutes after you log off. PM2 is the process manager:
sudo npm install -g pm2
pm2 start npm --name "myapp" -- start
pm2 save
pm2 startup # run the command it prints, then pm2 save again
That last line is the reboot insurance — it writes a systemd service so your app comes back after a restart. Then test it: pm2 list shows the process, pm2 logs myapp shows the output, and a rising restart count means a crash loop (check the logs, not your config).
The default port is 3000, but one app per port is the rule. A second app needs a different one: pm2 start npm --name "app2" -- start -- -p 3001, and your Nginx config has to point at the right port. "Port already in use" errors are almost always a stale process from a previous deploy that nobody killed.
Your update routine, forever: git pull && npm ci && npm run build && pm2 restart myapp. Builds, then restarts. Never in the same command chain — let the build finish first.
Step 4 — Nginx in front, Certbot on top
Never expose port 3000 to the internet. Nginx accepts public traffic on 80 and 443 and forwards it to Node. The config is small but the headers matter:
server {
listen 80;
server_name yourdomain.com;
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;
}
}
Skip X-Forwarded-Proto and Next.js can't tell HTTPS from HTTP behind the proxy, which produces redirect loops and broken absolute URLs — the kind of bug that only exists on the server, never locally.
Then SSL, which is the easiest part:
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
sudo certbot renew --dry-run
Certbot rewrites the Nginx config and sets up auto-renewal. The dry run confirms renewal will work without you finding out the hard way at 90 days.
Don't forget the firewall. Open 22, 80, and 443; close 3000. The app should only be reachable through Nginx. (If you haven't locked down the server yet, do the 30-minute VPS hardening before this goes anywhere near production.)
Step 5 — What breaks after your Next.js VPS deploy goes live
Once the chain is running, it runs. What breaks is the stuff around it:
- A 502 from Nginx is never an Nginx bug — it's a message that something behind it died. Check
pm2 logsfirst, then the Nginx error log. Restarting Nginx fixes nothing. (I wrote a full walkthrough of reading a 502 if you want the whole diagnostic order.) - New files under
public/sometimes don't serve after a deploy because the runningnext-servercaches the build manifest at startup. Apm2 restartafter adding covers or images is the fix. - Stale
.nextafter a killed build serves the old version silently. Always confirm the build exit code andBUILD_IDbefore restarting. - Watch memory over days. ISR caching grows the process over time;
pm2 monitand a one-line health check are all the monitoring a small app needs. - Keep secrets out of the client bundle. Only variables prefixed
NEXT_PUBLIC_should ever reach the browser. The official Next.js security checklist says the same: env files belong in.gitignore, public prefix only for public values.
The honest cost
A $6–22/month VPS runs a Next.js app, a database, and the reverse proxy side by side, with the same bill in January and December. The trade is one afternoon of setup and you owning the ops: logs, shell, and the power to fix things yourself instead of filing a ticket. For a throwaway demo, a managed platform is faster. For anything you'll run for a year, the VPS math wins — and the steps above are the whole of it: Node, build carefully, PM2, Nginx, SSL, and the firewall. Everything else is debugging.
References
- How to Host Nextjs App on a VPS in 2026 (vs Serverless) — Hostaccent
- Deploy Next.js on a VPS: Git, Nginx, PM2 & Production Setup (2026 Guide) — dev.to
- Deploying — Next.js official docs
- Production Checklist — Next.js official docs
- Security advice for self-hosting Next.js in Docker — Arcjet
- The Complete Guide to Self-Hosting Next.js at Scale — dlhck.com
Comments
More in Web Development

Is the AI Bubble About to Burst? The Honest 2026 Answer
The IMF just put a number on the AI-bubble debate: an investment bust could shave roughly a point off global GDP by 2028. This week gave both sides fresh ammunition — here is the honest middle.
Read more
