Next.js on PM2 — The Docker-Free Path
Deploy Next.js to a VPS with Node, PM2, and nginx — no containers required.
Why PM2 instead of Docker
Docker is the modern default, but it is not the only path. PM2 has been the Node-native production process manager for a decade: it runs your next start server, restarts it on crash, spawns one worker per CPU core, and does zero-downtime reloads on deploy. For a single-box Next.js deployment where the only thing on the server is a Node app, PM2 is lighter than Docker, starts faster, and uses ~100 MB less RAM. Pick Docker when you have multiple services and want isolation. Pick PM2 when it is just Node, nginx, and maybe a database.
Prepare Next.js for production
Two things to set in your app before you deploy: output: "standalone" in next.config.js (bundles only the files you need — smaller deploy, faster cold start), and a start script that binds to the right port:
// next.config.js
module.exports = {
output: 'standalone',
experimental: { serverActions: { bodySizeLimit: '2mb' } },
}
// package.json
{
"scripts": {
"build": "next build",
"start": "next start -p 3000"
}
}
Test locally: npm run build && npm run start. If it runs on port 3000, it will run on your VPS.
Install Node and PM2 on the server
On a fresh Ubuntu 24.04 VPS (already hardened from Lesson 2), install Node.js from NodeSource and PM2 globally:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
sudo npm install -g pm2
NodeSource pins a specific major — use setup_22.x for Node 22 LTS. Avoid the Ubuntu-packaged nodejs (it is usually years behind). Verify: node -v && pm2 -v.
Clone, build, and start with PM2
Deploy as your non-root deploy user. Clone into /srv/app, install dependencies, build, and launch with PM2:
cd /srv
sudo git clone https://github.com/you/your-app.git app
sudo chown -R deploy:deploy app
cd app
npm ci
npm run build
pm2 start npm --name "web" -- start
pm2 start npm --name "web" -- start runs your start script under the name web. Check it with pm2 status. Logs are at pm2 logs web — stream them for a minute to confirm startup is clean.
ecosystem.config.js: the real way
Ad-hoc pm2 start commands get lost. Commit an ecosystem.config.js to your repo so the deploy is reproducible:
module.exports = {
apps: [
{
name: 'web',
script: 'npm',
args: 'start',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
max_memory_restart: '512M',
error_file: '/var/log/pm2/web-error.log',
out_file: '/var/log/pm2/web-out.log',
},
],
}
instances: "max" spawns one worker per CPU core. exec_mode: "cluster" shares port 3000 across them via Node's built-in cluster module — you get parallelism without running multiple ports. max_memory_restart: "512M" respawns a worker if it leaks. Start it with pm2 start ecosystem.config.js.
Survive reboots: pm2 startup
By default, PM2 forgets your processes on reboot. Fix it once:
pm2 startup systemd -u deploy --hp /home/deploy
# Copy-paste the sudo command PM2 prints
pm2 save
This generates a systemd unit that starts PM2 on boot and immediately restores the saved process list. After any pm2 start / pm2 restart, run pm2 save again so the state persists. Miss this step and your app vanishes the next time the host reboots for kernel updates.
nginx in front for SSL and static assets
PM2 runs Next.js on localhost:3000. nginx terminates HTTPS on ports 80/443 and forwards to it. Install and configure in one sitting:
sudo apt install -y nginx certbot python3-certbot-nginx
# /etc/nginx/sites-available/yourdomain.com
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
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;
}
}
sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d yourdomain.com
certbot rewrites your nginx config to add the SSL block and sets up an auto-renewal cron. Done — HTTPS in thirty seconds.
Zero-downtime reload on deploy
The whole reason to use cluster mode is so you can reload without dropping requests. Your deploy script is four lines:
cd /srv/app
git pull origin main
npm ci --production=false
npm run build
pm2 reload ecosystem.config.js --update-env
pm2 reload restarts workers one at a time, waiting for each to be ready before killing the next. Requests in flight finish on the old worker; new requests go to the new one. To automate it, wire the four lines into a GitHub Action that SSH-es in on every push to main — see Lesson 9 for the workflow template. If you need to clear Next.js' build cache between deploys, rm -rf .next before npm run build.
Watch the three things that matter
pm2 monit gives you a live TUI with CPU, memory, and logs per worker. pm2 logs --lines 200 tails the most recent output across all apps. pm2 describe web prints the full config of the named app. For structured monitoring, PM2 Plus (paid) ships built-in dashboards, but a free netdata container (Lesson 10) covers 90% of what you actually watch. Set a memory alert at 80% of your VPS RAM — cluster mode with a memory leak eats a whole box fast.
Key takeaways
- PM2 is the Node-native alternative to Docker for single-service Next.js deploys
- Use `output: "standalone"` and an `ecosystem.config.js` for reproducible deploys
- `pm2 startup` + `pm2 save` is the step that makes your app survive reboots
- nginx + certbot give you HTTPS in one command — no Traefik required
- `pm2 reload` does zero-downtime rolling restart across cluster workers
Related documentation
Deploying from GitHub Repository
Deploy your own applications directly from GitHub with automatic framework detection.
Managing Environment Variables
Securely manage environment variables and secrets for your applications.
Adding a Domain to Your App
Configure a custom domain with automatic SSL certificate for your deployed application.
Understanding SSL Certificates
How Server Compass manages SSL certificates automatically with Let's Encrypt.