First PM2 Deploy — Next.js Standalone + Apache Reverse Proxy
I'd only installed PM2 before, on a Windows machine in order to run an Express.js server, which was a little annoying but mostly because I was new to Express. The process manager install itself wasn't horrible though, which is always a bonus.
Here's the setup, mostly so future-me doesn't have to reconstruct it from shell history.
Prerequisites
If it's really the first time, or it's a new box, go to the official Node.js website and follow the current instructions to download Node via the official NVM install script.
Then enable the Apache modules and restart:
sudo a2enmod proxy proxy_http headers rewrite
sudo systemctl restart apache2Clone the repo
cd /var/www/
git clone <your-repo-url> example
cd exampleInstalling PM2
Ubuntu's apt package for PM2 lags behind, so it's a global npm install instead:
npm install -g pm2Two commands make it survive a reboot. pm2 startup prints a sudo env PATH=... command specific to your system — run whatever it spits out, it registers a systemd service that relaunches PM2 itself on boot. Then, after starting your app for the first time:
pm2 startup
# run whatever that outputs
pm2 start ecosystem.config.js
pm2 savepm2 save snapshots the current process list so the systemd service knows what to bring back up. If another app gets added to this box under PM2 later, re-save after starting it.
PM2 configuration
This is the centralized configuration file used by PM2. Same category as next.config.ts. Bound to 127.0.0.1 only; Apache is the only thing that should ever talk to this port directly.
module.exports = {
apps: [
{
name: "portfolio",
script: ".next/standalone/server.js",
cwd: "/var/www/example",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
PORT: 3000,
HOSTNAME: "127.0.0.1",
},
autorestart: true,
max_restarts: 10,
},
],
};Apache vhost
Dropped in as its own file in /etc/apache2/sites-available rather than merged into anything existing — always do this — the server already runs other normal and proxied vhosts. Keep them separate so we don't have to scratch our heads figuring out which host went bad during an incident.
<VirtualHost *:80>
ServerName www.example.com
ServerAlias example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
RequestHeader set X-Forwarded-Proto "http"
</VirtualHost>Then sudo certbot --apache handles the cert and appends the :443 block — worth remembering to add X-Forwarded-Proto "https" to that block too, since Next.js cares about the original protocol for anything cookie- or absolute-URL-related.
sudo certbot --apache -d example.com -d www.example.comThe actual deploy
Once all of the above exists, deploys are one command:
git pull
npm ci
npm run build
cp -r public .next/standalone/public
cp -r .next/static .next/standalone/.next/static
pm2 reload ecosystem.config.jspm2 reload (not restart) is the one that matters for anything with actual visitors — it does a graceful, near-zero-downtime swap instead of a hard kill-and-relaunch.
The same steps live in deploy.sh.
That's the whole thing.