Node.js apps with PM2 and Nginx
A Node.js app started with a plain node app.js in an SSH session dies together with that session, and won't survive a crash or a server reboot. In this guide you'll install Node.js, run your app with the PM2 process manager (auto-restart on crash, auto-start after reboot), put Nginx in front of it as a reverse proxy, and confirm all of this survives a VPS reboot.
What you'll need
- An mHost VPS on Ubuntu 22.04/24.04, Debian 11/12, or AlmaLinux/Rocky Linux 8/9.
- SSH access as
rootor a user withsudorights. - Your Node.js app's code already uploaded to the server (for example, via
git cloneorscp) with apackage.jsonin it. The examples below use the path/var/www/myapp— replace it with your own. - (optional) a domain whose A record points at the server's IP — needed for
server_namein Nginx. You can skip this and use the server's IP address instead.
Step 1. Install Node.js
There are two standard ways to install Node.js on a VPS: a system package from the NodeSource repository (recommended for production — integrates with systemd and PM2 out of the box), or nvm (handy if you need multiple Node versions or an install without root).
Option A: via NodeSource (system-wide Node.js)
The current LTS line at the time of writing is Node.js 24.x. Substitute a different version for 24 if you need one (for example, 22 for the previous LTS line).
Ubuntu/Debian:
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt-get install -y nodejsAlmaLinux/Rocky Linux:
sudo dnf module disable -y nodejs
curl -fsSL https://rpm.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo dnf install -y nodejsVerify the install:
node -v
npm -vOption B: via nvm (no root, multiple Node versions)
nvm installs Node.js into the user's home directory — no root needed, and you can keep several versions side by side and switch between them.
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bashRe-open your SSH session, or run this for the current shell (bash):
source ~/.bashrcInstall and switch to the current LTS version:
nvm install --lts
nvm use --lts
node -vStep 2. Install PM2 and run your app
- Install PM2 globally:
sudo npm install -g pm2- Go to your app's directory and install its dependencies:
cd /var/www/myapp
npm ci- Start the app with PM2:
pm2 start app.js --name myapp- Check its status and logs:
pm2 status
pm2 logs myappmodule.exports = {
apps: [
{
name: "myapp",
script: "./app.js",
cwd: "/var/www/myapp",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
PORT: 3000
}
}
]
};Start it with: pm2 start ecosystem.config.js.
Step 3. Save the process list and enable PM2 startup
Until PM2's process list is saved and startup is configured, your app won't survive a server reboot.
- Generate the startup script:
pm2 startupPM2 detects the init system in use (systemd) and prints a ready-made command, roughly like this:
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u root --hp /rootCopy and run exactly the command PM2 printed for you — the PATH, the user (-u), and the home directory (--hp) will be filled in for your system.
- Save the current process list:
pm2 saveStep 4. Set up Nginx as a reverse proxy
- Install Nginx and enable it on boot:
Ubuntu/Debian:
sudo apt-get install -y nginx
sudo systemctl enable --now nginxAlmaLinux/Rocky Linux:
sudo dnf install -y nginx
sudo systemctl enable --now nginx- Create a site config — the path depends on the distro.
Debian/Ubuntu: sites-available/sites-enabled
sudo nano /etc/nginx/sites-available/myappAlmaLinux/Rocky Linux: conf.d
sudo nano /etc/nginx/conf.d/myapp.confThe file's contents are the same for both:
server {
listen 80;
server_name example.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;
}
}Replace example.com with your own domain or the server's IP, and 127.0.0.1:3000 with the address and port your app listens on.
On Debian/Ubuntu, also enable the site via a symlink:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/Test the configuration and reload it:
sudo nginx -t
sudo systemctl reload nginxsudo setsebool -P httpd_can_network_connect onStep 5. Open the ports in the firewall
Port 80 (and 443, if you add HTTPS later) needs to be open externally. There's no need to open the app's own port (3000 in the example) in the external firewall — only Nginx, on the same server, talks to it.
Ubuntu/Debian (ufw):
sudo ufw allow 80,443/tcpSee the ufw firewall guide for details.
AlmaLinux/Rocky Linux (firewalld):
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reloadStep 6. Confirm everything survives a reboot
- Make sure both Nginx and PM2 are enabled to start on boot:
systemctl is-enabled nginx
systemctl is-enabled pm2-rootBoth should print enabled (replace root with your actual user if you ran pm2 startup as someone else).
- Test it for real — reboot the server:
sudo rebootWait a minute or two, reconnect over SSH, and check:
pm2 status
curl -I http://127.0.0.1:3000
curl -I http://localhost/
systemctl status nginxIf pm2 status shows your app as online and both curl commands return an HTTP response, everything is set up correctly.
What's next
The usual next step is adding HTTPS (for example, via Let's Encrypt/certbot) and opening the rule for 443 in the firewall (see Step 5). If you run several apps on the same server, give each one its own Nginx config and its own entry in ecosystem.config.js, and remember to run pm2 save after any change to the process list.