Skip to content
Back to blog
  • 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.

MMuhammad Muneeb1 min read

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.

ecosystem.config.cjs
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 portfolio

Put 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.

/etc/nginx/sites-available/example.com
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

Engineering1 min read

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 article