← Back to Blog
GeneralWeb DevelopmentServer Administration

How to Host Multiple Next.js Apps on One VPS with PM2 and Nginx (Without Port Conflicts)

By JustinPublished September 11, 202661 views
Diagram of multiple Next.js apps behind Nginx on one VPS, each managed by PM2

I run four separate Next.js sites off a single DigitalOcean droplet — a coupon and course-discovery site, a news site, a hotel website, and a couple of internal tools. They all share one server, one Nginx install, and one PM2 process manager. Most of the time this works well: it's cheap, it's simple to reason about, and I don't need four separate servers for four low-to-medium traffic sites. The one recurring failure mode is a port conflict — two apps configured to listen on the same port, so one of them crash-loops in PM2 while the other stays up. I've hit this more than once, most recently when a new app's PM2 config still had the port setting copied from an older project. This guide is the exact setup I use to avoid that, plus how to actually diagnose it when it happens anyway.

The Quick Answer

  • Give every app its own dedicated port (3000, 3001, 3002…) — never let two apps default to the same one
  • Run each app under PM2 with an ecosystem.config.js that pins the port explicitly, not just in package.json
  • Point each domain at its app through an Nginx server block that reverse-proxies to localhost:
  • When something won't start, check pm2 logs first — port conflicts show up immediately as EADDRINUSE
Now the full setup.

Why This Approach (Instead of Docker or Separate VPS's)

Before the how-to: why bother with PM2 + Nginx instead of just containerizing everything or renting a separate droplet per site? For a handful of low-to-moderate traffic sites, a single VPS running multiple PM2 processes behind Nginx is dramatically cheaper and simpler to operate than the alternatives. Docker adds real value once you have a team, CI/CD, or need to guarantee environment parity across machines — for one person managing a handful of Next.js sites, it's often more overhead than it's worth. A separate VPS per site multiplies your hosting bill for sites that individually use a fraction of a server's capacity. The trade-off is that everything shares the same machine's resources and the same process manager's configuration — which is exactly why port management has to be deliberate rather than assumed.

Step 1: Give Every App Its Own Port

next start with no flags defaults to port 3000. If you have more than one Next.js app on the same server and you don't explicitly assign ports, the second one you start will either fail outright or — worse — silently conflict with the first depending on how it's started. I keep a simple mental map of which app owns which port:
  • App A → 3000
  • App B → 3006
  • App C → 3010
  • App D → 3020
There's no requirement that these be sequential or close together — I actually prefer leaving gaps between them, so that if App B ever needs a second process (for a worker or a staging instance) I can slot it in at 3007 or 3008 without renumbering everything else. Set the port in your package.json start script so it's explicit rather than relying on a default:
json
{
  "scripts": {
    "start": "next start -p 3006"
  }
}

Step 2: Configure PM2 with an Explicit Port in the Environment

This is the step that actually caused a real outage for me. I have a shared ecosystem.config.js pattern for each app, and it's tempting to copy one app's config as a starting point for the next one. If you copy the file and forget to change the PORT value in the env block, PM2 will happily start the new app — and it will immediately crash, because the port is already taken by the app you copied from. Here's the pattern I use, one file per app:
js
// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'app-b',
    script: 'npm',
    args: 'start',
    cwd: '/var/www/app-b',
    instances: 1,
    exec_mode: 'fork',
    max_memory_restart: '1200M',
    node_args: '--max-old-space-size=1024',
    env: {
      NODE_ENV: 'production',
      PORT: 3006,          // <-- this is the line to double-check every time
      HOSTNAME: '0.0.0.0',
    },
  }],
};
The PORT value here is what actually wins at runtime for most Next.js start scripts — if it doesn't match what you put in package.json, the environment variable takes precedence. That mismatch is a second, sneakier way to end up with a conflict: the package.json script says -p 3006, but the ecosystem file's env.PORT still says 3000 from a copy-paste, and the app binds to 3000 anyway. The fix, if you ever hit this: don't just pm2 restart — a restart reuses the already-loaded environment and won't pick up a changed PORT value. You need to fully remove and re-add the process:
bash
pm2 delete app-b
pm2 start ecosystem.config.js
pm2 save

Step 3: Diagnosing a Port Conflict When It Happens

The symptom is always the same: pm2 list shows the app status as errored, and it will show a high restart count because PM2 keeps trying and failing in a tight loop.
bash
pm2 list
# app shows status: errored, restarts climbing rapidly
 
pm2 logs app-b --lines 30
The log will show something like:
text
Error: listen EADDRINUSE: address already in use :::3000
That tells you exactly what's wrong — something else already has that port. Find out what:
bash
sudo lsof -i :3000
# or
sudo ss -tulpn | grep :3000
This gives you the PID and process name currently bound to that port. From there it's usually one of two situations: a genuinely different app is supposed to be on that port (fix the conflicting app's config instead), or it's an orphaned process from an earlier crash that never released the port (kill it and restart cleanly).

Step 4: Route Each Domain to Its App with Nginx

Once every app has its own port and is confirmed running (pm2 list shows online, not errored), Nginx is what maps a public domain name to the right internal port. Each app gets its own server block:
nginx
server {
    listen 80;
    server_name appb.example.com www.appb.example.com;
 
    location / {
        proxy_pass http://localhost:3006;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_cache_bypass $http_upgrade;
    }
}
The Upgrade/Connection headers matter specifically for Next.js — they're what let WebSocket connections (used by Next's dev fast-refresh and some app features) pass through the proxy correctly. Save this in /etc/nginx/sites-available/appb.example.com, symlink it into sites-enabled, then test and reload:
bash
sudo ln -s /etc/nginx/sites-available/appb.example.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
nginx -t is worth running every time before reloading — it validates the config syntax and will catch a typo before it takes down every site on the server, not just the one you're editing.

Step 5: Add HTTPS with Certbot

Once the plain-HTTP routing works, adding a free TLS certificate for each domain is one command per domain with Certbot:
bash
sudo certbot --nginx -d appb.example.com -d www.appb.example.com
Certbot edits the Nginx server block automatically to add the certificate paths and a redirect from port 80 to 443. It also sets up a renewal cron job, so this is generally a one-time step per domain.

Keeping Track as You Add More Apps

Once you're past two or three apps, an ad-hoc mental map of ports stops being reliable. I keep a plain text file on the server itself — /root/PORTS.md — listing every app, its port, its PM2 process name, and its domain. It takes thirty seconds to update when adding a new app and has saved me from at least one repeat of the exact conflict this guide describes. A minimal version:
text
app-a    → port 3000 → pm2 name: app-a  → domain: appa.example.com
app-b    → port 3006 → pm2 name: app-b  → domain: appb.example.com
app-c    → port 3010 → pm2 name: app-c  → domain: appc.example.com

Frequently Asked Questions

Can two Next.js apps share the same port if they're on different domains?

No — the port is what Nginx proxies to internally, and it's independent of the public domain. Two apps on the same port will conflict regardless of what domain name points to them, because the operating system only allows one process to bind to a given port at a time.

Do I need Docker to do this safely?

No. Docker can make port isolation more automatic (each container can bind to the same internal port and be mapped to different host ports), but it's not required. Explicit, well-documented port assignment in PM2 configs achieves the same isolation without the added complexity of container orchestration for a small number of apps.

What happens if I restart the server — do my apps come back automatically?

Only if you've run pm2 save after starting your apps, and set up PM2's startup script with pm2 startup (which configures a systemd service to resurrect your saved process list on boot). Without both steps, a server reboot will leave all your apps stopped.

How many Next.js apps can realistically run on one small VPS?

This depends entirely on traffic and the memory/CPU size of the droplet, not on some fixed app-count limit. On a modest droplet (a few GB of RAM), three to five low-to-moderate-traffic Next.js apps running in PM2's fork mode is a reasonable range before you should start watching memory pressure closely with pm2 monit or considering a bigger instance.

Is PM2's cluster mode worth using here?

Cluster mode runs multiple instances of one app across CPU cores for higher throughput on a single app — it solves a different problem than hosting multiple separate apps. For most personal or small-business multi-site setups, fork mode (one process per app) is simpler to reason about and sufficient.

Tags:Next.jsPM2NginxVPSDevOpsNode.jsreverse proxy

Justin is a self-taught developer who builds and runs DeelCart himself — from the articles to the server it runs on. He manages his own Linux infrastructure and writes guides based on tools and workflows he actually uses day to day.

✍️ More Guides on DeelCart

Read more of our shopping and learning guides.

Browse the Blog →