Deploying Next.js on a VPS: The 12 Things Nobody Tells You
DEV Community

Deploying Next.js on a VPS: The 12 Things Nobody Tells You

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 npm start Running npm start 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. npm install -g pm2 // ecosystem.config.js module.exports = { apps: [{ name: "my-app", script: "node_modules/.bin/next", args: "start", cwd: "/var/www/my-app", instances: 1, exec_mode: "fork", autorestart: true, max_memory_restart: "512M", env: { NODE_ENV: "production", PORT: 3000 }, }], }; cd /var/www/my-app && pm2 start ecosystem.config.js pm2 save pm2 startup systemd -u YOUR_USER --hp /home/YOUR_USER 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: upstream nextjs_upstream { server 127.0.0.1:3000; keepalive 64; } server { listen 80; server_name example.com www.example.com; location / { proxy_pass http://nextjs_upstream; 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; # WebSocket support - required for HMR and any realtime features proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } 3. Forgetting the WebSocket upgrade headers breaks more than dev mode proxy_set_header Upgrade $http_upgrade; and Connection "upgrade" 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. 4. client_max_body_size - 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 413 Request Entity Too - and only in production, since your local dev server has no such Large limit. Set it explicitly: client_max_body_size 25M; 5. proxy_buffering off 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: proxy_buffering off; 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: dig +short example.com # should print your server's public IP 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: sudo ufw allow OpenSSH sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable # only after the rules above are in place 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 sudo certbot --nginx -d example.com -d www.example.com \ --agree-tos -m y**@example.com --redirect This adds the SSL server block, the HTTPโ†’HTTPS redirect, and the certificate paths automatically. Run nginx -t afterward to confirm it's still valid, and glance at /etc/nginx/sites-available/example.com 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: sudo systemctl status certbot.timer sudo certbot renew --dry-run 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 (/var/log/auth.log ). 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. # /etc/fail2ban/jail.d/sshd.local [sshd] enabled = true backend = systemd maxretry = 4 findtime = 10m bantime = 1h Verify it's actually watching: sudo fail2ban-client status sshd 11. next/image is agonizingly slow without sharp On Vercel, image optimization just works. On a raw VPS, if you use the component without installing sharp , Next.js falls back to a purely JavaScript-based image optimizer. It is incredibly slow and eats up your CPU. npm install sharp 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 next build 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: sudo fallocate -l 2G /swapfile sudo chmod 600 /swapfile sudo mkswap /swapfile sudo swapon /swapfile echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab 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. Top comments (0)

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.