PM2 is a process manager for Node.js that keeps applications running, restarts them when they crash, spreads them across CPU cores in cluster mode and collects their logs. In this tutorial you will deploy a small Express application on Ubuntu 24.04 with PM2, describe it in an ecosystem file, make PM2 start it automatically at boot through systemd, rotate its logs, and publish it through Nginx. You will also run a zero-downtime reload, which is how you roll out new versions.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. This guide usesyour_userin paths; replace it with your username. - Node.js and npm installed system-wide, for example Node.js 24 LTS from the NodeSource repository. Check with
node --version. - Optionally, a domain name with an A record pointing to
your_server_ipif you want to serve the app by name.
Step 1 - Installing PM2
Install PM2 globally so the pm2 command is available to every user:
sudo npm install -g pm2
Verify the installation:
pm2 --version
6.0.13
The first time you run a pm2 command as your user, PM2 starts its daemon and creates ~/.pm2, where it keeps logs, PID files and the saved process list. Run PM2 as your normal user, not with sudo, so the daemon and the apps it launches do not run as root.
Step 2 - Creating a sample application
If you already have an application, you can skip to Step 3 and use its entry point instead. Otherwise, create a minimal Express API to follow along:
mkdir -p ~/apps/my-api
cd ~/apps/my-api
npm init -y
npm install express
Create the entry point:
nano app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({ status: 'ok', env: process.env.NODE_ENV, pid: process.pid });
});
const server = app.listen(port, '127.0.0.1', () => {
console.log(`my-api listening on 127.0.0.1:${port}`);
});
// Let PM2 stop or reload the process cleanly
process.on('SIGINT', () => {
server.close(() => process.exit(0));
});
The SIGINT handler matters: when PM2 reloads or stops a process it sends SIGINT first, and closing the server lets in-flight requests finish before the process exits.
Step 3 - Writing an ecosystem file
You can start an app with pm2 start app.js, but an ecosystem file keeps every option in version control and makes restarts reproducible. Create it in the project root:
nano ~/apps/my-api/ecosystem.config.js
module.exports = {
apps: [
{
name: 'my-api',
script: './app.js',
cwd: '/home/your_user/apps/my-api',
instances: 'max',
exec_mode: 'cluster',
max_memory_restart: '300M',
time: true,
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
},
],
};
What these options do:
instances: 'max'withexec_mode: 'cluster'starts one worker per CPU core. All workers share port 3000; PM2 balances connections between them, so you do not need one port per instance.max_memory_restartrestarts a worker that grows beyond 300 MB, a safety net against memory leaks.time: trueprefixes every log line with a timestamp.env_productionis applied when you pass--env production.
Start the app with the production environment:
cd ~/apps/my-api
pm2 start ecosystem.config.js --env production
List the processes:
pm2 ls
PM2 prints a table with one row per worker. On a 2 vCPU server you will see two rows named my-api, both with mode cluster and status online. Send a few requests and notice that the pid changes as PM2 distributes them:
curl http://127.0.0.1:3000
{"status":"ok","env":"production","pid":14231}
Step 4 - Starting PM2 at boot
PM2 can generate a systemd unit that starts the daemon at boot and restores the processes you saved. Run:
pm2 startup systemd
Because you are not root, PM2 does not change the system itself. It prints the exact command to run:
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u your_user --hp /home/your_user
Copy and run the command that PM2 printed on your server. It creates and enables a unit named pm2-your_user.service.
Now save the current process list. This is what PM2 restores at boot:
pm2 save
Check the systemd unit:
systemctl status pm2-your_user
● pm2-your_user.service - PM2 process manager
Loaded: loaded (/etc/systemd/system/pm2-your_user.service; enabled; preset: enabled)
Active: active (running) since ...
To confirm the full cycle, reboot with sudo reboot, reconnect and run pm2 ls. Both workers should be online again. Remember to run pm2 save again every time you add or remove apps.
Step 5 - Managing logs
PM2 writes each app's standard output and error streams to ~/.pm2/logs/. Follow them live:
pm2 logs my-api --lines 20
Press CTRL+C to stop following. These files grow forever by default, so install the pm2-logrotate module:
pm2 install pm2-logrotate
Configure it to rotate at 10 MB, keep 14 files and compress old ones:
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 14
pm2 set pm2-logrotate:compress true
Check the module settings:
pm2 conf pm2-logrotate
The module runs as its own PM2 process, so pm2 ls now shows it under a separate Module section. Run pm2 save again so it is restored after a reboot.
For a live view of CPU and memory per worker, use pm2 monit.
Step 6 - Deploying updates with zero downtime
In cluster mode, pm2 reload restarts workers one at a time and waits for each new worker to come online before stopping the next old one, so the app keeps accepting connections during a deploy. A typical update from a Git checkout looks like this:
cd ~/apps/my-api
git pull
npm ci --omit=dev
pm2 reload ecosystem.config.js --env production --update-env
npm ci installs exactly what package-lock.json specifies, --omit=dev skips development dependencies, and --update-env makes PM2 apply any environment variables you changed in the ecosystem file.
To test the reload, run a request loop in a second SSH session:
while true; do curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000; sleep 0.2; done
Run pm2 reload my-api in the first session. The loop should keep printing 200 without any connection errors. Stop it with CTRL+C.
Note
pm2 restartstops and starts all workers at once and causes a short outage. Usereloadfor deployments.
Step 7 - Publishing the app through Nginx
The app listens only on 127.0.0.1, so it is not reachable from outside. Nginx will accept public traffic on port 80 and proxy it to PM2. Install Nginx:
sudo apt install nginx
Create a server block:
sudo nano /etc/nginx/sites-available/my-api
server {
listen 80;
server_name your_domain;
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Replace your_domain with your domain name, or with your_server_ip if you do not have one. The Upgrade and Connection headers let WebSocket connections pass through.
Enable the site, test the configuration and reload Nginx:
sudo ln -s /etc/nginx/sites-available/my-api /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
If UFW is active, allow HTTP and HTTPS:
sudo ufw allow 'Nginx Full'
From your local machine, open http://your_domain. You should see the JSON response with "env":"production". To add HTTPS, install Certbot with sudo apt install certbot python3-certbot-nginx and run sudo certbot --nginx -d your_domain.
Troubleshooting
The app shows errored or keeps restarting in pm2 ls. Read the error log, which usually contains the stack trace:
pm2 logs my-api --err --lines 50
A common cause is EADDRINUSE, meaning another process already uses port 3000. Find it with sudo ss -ltnp 'sport = :3000'.
Apps do not come back after a reboot. Check that the unit is enabled with systemctl is-enabled pm2-your_user and that you ran pm2 save after the last change. If you upgraded Node.js or moved PM2, regenerate the unit with pm2 unstartup systemd, then repeat Step 4.
pm2 ls shows no apps, but the site still works. You are probably looking at another user's PM2 daemon, for example after running sudo pm2. Each user has a separate daemon in their own ~/.pm2. Always run PM2 as the user that owns the app.
Nginx returns 502 Bad Gateway. Nginx cannot reach the app. Confirm it answers locally with curl http://127.0.0.1:3000 and check sudo tail -n 20 /var/log/nginx/error.log.
Conclusion
Your Node.js app now runs under PM2 in cluster mode, starts automatically at boot through a systemd unit, rotates its logs and accepts public traffic through Nginx. You also have a repeatable deploy procedure that reloads workers without downtime.
As next steps you can:
- Secure the site with a Let's Encrypt certificate and redirect HTTP to HTTPS.
- Move secrets out of the ecosystem file into environment variables loaded from a file only your user can read.
- Automate the Step 6 commands from your CI pipeline over SSH.
