Moving a Next.js app off Vercel and onto a plain Ubuntu VPS usually starts with a painful realization: either your serverless functions are timing out on background jobs, or your client just handed you a strict "you must host this on our infrastructure" requirement.
Deploying the app itself is easy.
What trips people up (and what cost me hours of debugging and locking myself out of my own server) is everything around the app.
Here are the 12 things that actually break when you leave the serverless ecosystem, in the order you'll hit them.
1.
Next.js needs a process manager, not just Running in a terminal dies the moment you disconnect.
You need something that keeps the process alive, restarts it on crash, and survives a reboot.
PM2 is the simplest option for a single-server Node deploy.
That last line is the one people forget - without it, PM2's process list doesn't survive a server reboot.
2.
Nginx needs to proxy to the port, not serve the files Next.js is not a static site (unless you've explicitly exported it as one).
Nginx's job is to forward requests to the Node process, not serve files from disk:
3.
Forgetting the WebSocket upgrade headers breaks more than dev mode and aren't just for Fast Refresh in development.
Any app using WebSockets or Server-Sent Events in production (chat, live notifications, streaming AI responses) silently breaks without these two lines.
This is one of the most common "works locally, broken in prod" bugs.
- - the silent 413 error Nginx defaults to a 1MB request body limit.
File uploads, image processing endpoints, and some API routes will fail with a - and only in production, since your local dev server has no such limit.
Set it explicitly: - for streaming responses If you're streaming a response (Server-Sent Events, streaming LLM completions via the Vercel AI SDK, etc.), Nginx's default buffering will hold the entire response before sending it to the client - defeating the purpose of streaming.
Turn it off for the relevant location block:
6.
DNS propagation is the #1 cause of "SSL setup failed" Before running Certbot, verify your domain's A record actually points at the server: If it doesn't match, Certbot's HTTP-01 challenge will fail - not because of a config error, but because Let's Encrypt can't reach your server at that domain yet.
This single check saves more support tickets than anything else on this list.
7.
UFW must allow SSH before you enable it This one is a rite of passage: Enable UFW before allowing SSH and you'll lock yourself out of your own server.
Always add the SSH rule first.
8.
Certbot's Nginx plugin edits your config for you - read the diff This adds the SSL server block, the HTTP→HTTPS redirect, and the certificate paths automatically.
Run afterward to confirm it's still valid, and glance at once so you know what changed.
9.
Certbot renewal isn't automatic until you verify it Certbot installs a systemd timer, but "installed" isn't the same as "working." Verify it explicitly: If the dry run fails, your certificate will expire in 90 days without you knowing until browsers start showing warnings to your users.
10.
Fail2Ban's default backend breaks on Ubuntu 24.04 Ubuntu 22.04 and earlier used file-based auth logs (). 24.04 moved logging fully to the systemd journal.
If your Fail2Ban jail is still configured for the file backend, it silently monitors a log that barely gets written to - and bans nothing.
Verify it's actually watching: - is agonizingly slow without On Vercel, image optimization just works.
On a raw VPS, if you use the component without installing , Next.js falls back to a purely JavaScript-based image optimizer.
It is incredibly slow and eats up your CPU.
Add it to your production dependencies.
If you don't, your $6 VPS will easily spike to 100% CPU utilization just trying to serve a few optimized avatars.
12.
Your app builds fine locally and OOMs on a 1GB VPS on a $6/month 1GB RAM droplet frequently gets killed by the OOM reaper mid-build - with no clear error, just a dead process.
If you don't have a CI pipeline building elsewhere, add swap before your first build: The checklist, if you're tired of doing this manually I got tired of locking myself out of UFW, forgetting to set the WebSocket headers, and missing the systemd fixes.
So, I bundled this exact Next.js setup (Nginx vhost, UFW, Fail2Ban, Swap, and Certbot) into a single bash script.
I also added one thing this checklist can't: a Telegram notification the moment anyone logs into the server over SSH.
You can grab the ServerSecure Setup here.
This article is the complete free version of the checklist - nothing here is paywalled.
The script is just for when you'd rather spend those 15 terminal commands building your actual product.
Running into something not covered here?
Drop it in the comments - I'll add it to the list.