- DevOps
- Engineering
Self-Hosting Next.js on a VPS with PM2 and Nginx
You don’t need a platform to run Next.js well. Here’s how I deploy it on a plain VPS — PM2 for the process, Nginx out front, and a one-command deploy.
Managed platforms are great until you want control over cost, data, or runtime. For a lot of my projects a single VPS running the Next.js server behind Nginx is simpler, cheaper, and completely predictable. Here’s the setup I reach for.
Run the server under PM2
Next.js in production is just a long-running Node process. PM2 keeps it alive, restarts it on crashes and on reboot, and gives you logs. I bind it to localhost so only Nginx can reach it.
module.exports = {
apps: [
{
name: 'portfolio',
script: 'node_modules/next/dist/bin/next',
args: 'start --hostname 127.0.0.1 --port 4090',
instances: 1,
exec_mode: 'fork',
env: { NODE_ENV: 'production' },
max_memory_restart: '600M',
autorestart: true,
},
],
}Then it’s just:
pm2 start ecosystem.config.cjs
pm2 save # persist across reboots
pm2 logs portfolioPut Nginx in front
Nginx terminates TLS (I use a Cloudflare origin certificate), serves immutable static assets with long cache headers, and reverse-proxies everything else to the Node process. The important bit is forwarding the right headers so the app sees the real protocol and client IP.
location /_next/static/ {
proxy_pass http://127.0.0.1:4090;
expires 365d;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://127.0.0.1:4090;
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;
}Make deploys boring
A deploy is: pull the latest code, install, build, then reload PM2. I wrap it in a single script with change-detection so re-running is cheap, and run it over SSH from CI so a push to main ships itself.
- Build first, swap second — never leave a half-built app serving traffic.
- Keep uploaded media and the .env outside the build so a deploy never wipes them.
- Health-check after reload; roll back if the process doesn’t come up.
Owning the box means owning the boring parts too — but a good deploy script turns them into one command.
That’s the whole stack: PM2 for the process, Nginx for the edge, a script to tie it together. No magic, no lock-in, and it scales vertically a long way before you need anything fancier.
Related posts
Background Jobs in Node.js with BullMQ and Redis
Slow work doesn’t belong in a request. Offload it to a queue. Here’s how I structure reliable background jobs in Node.js with BullMQ and Redis.
Read articleDesigning a Scalable Serverless API on AWS Lambda
How I structure Lambda + API Gateway + DynamoDB services that stay cheap, fast and maintainable as they grow.
Read articleBuilding Custom Shopify Apps with the Admin GraphQL API
A practical walkthrough of building embedded Shopify apps with Remix, Polaris and the Admin GraphQL API.
Read article