POWER
MENU
Kindle
·6 min read·

Deploying Next.js to a VPS: Nginx, PM2, and Custom Domains

A practical guide to deploying Next.js applications on a VPS with Nginx reverse proxy, PM2 process management, and custom domain setup.

nextjsvpsdeploymentnginxpm2devops

Deploying a Next.js application to a VPS gives you control that managed platforms can't match. You decide the runtime, the caching strategy, the security posture, and the cost. The trade-off is you also decide the failure modes. I've run production Next.js apps on a $5/month VPS for years, and the setup is simpler than most tutorials make it seem.

Why a VPS over Vercel or Netlify

Managed platforms abstract away the infrastructure, which is great until you need something they don't support. Custom middleware, specific Node.js flags, persistent background jobs, or just wanting to avoid vendor lock-in are all valid reasons to run your own server. A VPS also costs less at scale. A $5/month VPS can handle tens of thousands of monthly visitors for a typical Next.js app, while equivalent traffic on Vercel's Pro plan runs $20 or more.

The downside is you become the operations team. You monitor the server, apply security patches, and debug outages. If that sounds like work you're willing to do, the rest of this guide will get you there.

Environment setup

Start with a fresh Ubuntu 22.04 VPS. I use the smallest instance from most providers—1GB RAM, 1 vCPU, 25GB SSD. That's enough for a Next.js app and a few side services.

bash
Loading...
Loading syntax highlighting...

Verify Node.js is installed correctly:

bash
Loading...
Loading syntax highlighting...

Application setup

Clone your Next.js repository. I keep mine in /var/www/ but any directory works as long as the user running PM2 has read access.

bash
Loading...
Loading syntax highlighting...

Install dependencies and build. Use npm ci for reproducible builds instead of npm install.

bash
Loading...
Loading syntax highlighting...

The build step is critical. Next.js 15+ with Turbopack can have memory issues on small VPS instances. If the build fails with out-of-memory errors, add a swap file:

bash
Loading...
Loading syntax highlighting...

PM2 process management

PM2 keeps your application running, restarts it on crashes, and manages logs. Create a simple ecosystem file:

javascript
Loading...
Loading syntax highlighting...

Start the application:

bash
Loading...
Loading syntax highlighting...

The last command generates a systemd service that starts PM2 on boot. Follow the output to enable it.

Check that your app is running:

bash
Loading...
Loading syntax highlighting...

Nginx reverse proxy

Nginx sits in front of your Next.js app, handling SSL termination, static file serving, and load balancing if you scale to multiple instances.

Create a new Nginx config:

bash
Loading...
Loading syntax highlighting...

Paste this configuration:

nginx
Loading...
Loading syntax highlighting...

Enable the site and test the config:

bash
Loading...
Loading syntax highlighting...

Custom domain and SSL

Point your domain's A record to your VPS IP. I use Cloudflare because their DNS is fast and free. Once DNS propagates, install Certbot for Let's Encrypt SSL certificates:

bash
Loading...
Loading syntax highlighting...

Certbot will modify your Nginx config automatically and set up automatic renewal. Test the renewal with:

bash
Loading...
Loading syntax highlighting...

Common pitfalls

Port conflicts: Make sure nothing else is using port 3000. Use sudo lsof -i :3000 to check.

File permissions: The user running PM2 needs read access to your app directory. I create a dedicated system user for each app:

bash
Loading...
Loading syntax highlighting...

Then update the PM2 ecosystem file to run as that user.

Memory limits: Next.js builds can be memory-hungry. If your build fails, check dmesg for OOM killer logs. The swap file helps, but consider upgrading to a VPS with more RAM if builds consistently fail.

Nginx caching: The static file caching in the Nginx config is aggressive. If you change your CSS or JavaScript and don't see updates, clear the browser cache or add a version query string to your assets.

PM2 logs: Check application logs with pm2 logs nextjs-app. Errors often appear there before they reach the browser.

Monitoring

Basic monitoring is essential. I install htop for real-time resource viewing and set up a simple uptime check with cron:

bash
Loading...
Loading syntax highlighting...

For more serious monitoring, I run a Prometheus + Grafana stack on the same VPS, but that's a topic for another post.

The control trade-off

Running your own VPS means you control everything, from the Node.js version to the SSL cipher suites. That control comes with responsibility—you're on call when things break. But for many developers, that's a fair trade. You learn how the stack actually works, you can optimize for your specific use case, and you're not at the mercy of a platform's pricing changes or feature deprecations.

My $5/month VPS has hosted portfolio sites, small SaaS applications, and internal tools for years. It rarely goes down, and when it does, I know exactly how to fix it because I built the system. That knowledge is worth more than the time saved by using a managed platform.

Start with a non-critical project. Deploy a personal blog or a side project. Get comfortable with the commands, the failure modes, and the recovery procedures. Once you've survived your first midnight outage fix, you'll know whether the VPS path is right for you.

Related Posts