---
title: "How to deploy a Node.js app on a VPS: PM2, nginx and systemd"
description: "Node.js on an Ubuntu or Debian VPS: installing the LTS release, a dedicated user, .env secrets, running under PM2 or systemd, nginx in front and zero-downtime updates."
url: https://tihost.io/en/blog/nodejs-app-on-vps
language: en
section: "Guides"
published: 2026-10-05
updated: 2026-10-05
publisher: Tihost (https://tihost.io)
---

# How to deploy a Node.js app on a VPS: PM2, nginx and systemd

> **In short:** To keep a Node.js app running 24/7 on a VPS, install the current Node.js LTS release from the NodeSource repository, create a dedicated user for the app, move secrets into a .env file and run the process under PM2: `pm2 start`, then `pm2 save` and `pm2 startup`. nginx faces the internet on ports 80 and 443, and new code ships without downtime via `pm2 reload`.

**Key takeaways:**

- Install Node.js on a server from the NodeSource repository or with nvm: the `nodejs` package in the stock Ubuntu and Debian repositories is usually well behind the current LTS release.
- PM2 keeps a Node.js app running 24/7: `pm2 start` launches the process, `pm2 save` stores the process list, and `pm2 startup` brings it back after a server reboot.
- A Node.js app listens on a local port (for example 3000), while nginx faces the internet on ports 80 and 443 as a reverse proxy - HTTPS is configured there too.
- In cluster mode `pm2 reload` restarts Node.js processes one at a time, so a code update causes no downtime, whereas `pm2 restart` and `systemctl restart` cause a short gap.
- A small API or website on Node.js fits a VPS with 1 vCPU and 2 GB of RAM - on Tihost that configuration costs $4.00 a month.

> Commands target Ubuntu 22.04/24.04 and Debian 12. Do the [first server setup](https://tihost.io/en/blog/vps-first-setup) before you start: the commands below run as a sudo user, not as root.

## Step 1. Install the current Node.js LTS release

LTS (Long Term Support) is the Node.js release line with long-term security fixes, and it is the one to run on a server. The main route is the NodeSource repository: the `setup_lts.x` script adds it for the current LTS line, after which Node.js and npm install and update through plain `apt`. The script is downloaded to a file first so you can read it before running it:

```bash
sudo apt update
sudo apt install -y ca-certificates curl git
curl -fsSL https://deb.nodesource.com/setup_lts.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -v
npm -v
```

The second route is nvm (Node Version Manager): it installs Node.js into a user's home directory with `nvm install --lts` and is handy when one server needs several versions. The downside on a server is that the path to `node` depends on the user and version, so you have to spell it out in systemd units and cron by hand. LTS release lines and their support schedule are listed on [nodejs.org](https://nodejs.org/).

## Step 2. Create a dedicated user for the app

The app should not run as root: if someone finds a vulnerability in it, they only get that user's rights. The `app` user has no password, so it cannot log in over SSH by itself, and you switch to it with `sudo -iu app`:

```bash
sudo adduser --disabled-password --gecos "" app
sudo -iu app
```

## Step 3. Get the application code onto the server

git is the easiest way to deliver code: clone the repository once, then update with `git pull`. `npm ci` installs dependencies exactly as pinned in `package-lock.json`, and `npm run build --if-present` builds the project if it has a `build` script (TypeScript, Next.js, Nest):

```bash
git clone https://github.com/you/myapp.git ~/myapp
cd ~/myapp
npm ci
npm run build --if-present
```

For a private repository, give the `app` user an SSH key (`ssh-keygen -t ed25519`) and add the public half to the repository as a read-only deploy key - the server then needs neither your password nor a personal token. More on keys in the guide to [connecting to a VPS over SSH](https://tihost.io/en/blog/ssh-connect-to-vps).

## Step 4. Move settings and secrets into .env

Passwords, tokens and database URLs do not belong in code or in git - they go into a `.env` file that only its owner can read. Add `.env` to `.gitignore` so it never ends up in the repository:

```bash
cat > ~/myapp/.env <<'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://myapp:password@localhost:5432/myapp
EOF
chmod 600 ~/myapp/.env
```

The app has to read `.env` itself: most often through the dotenv package (`npm install dotenv` and `require("dotenv").config()` as the first line), and Node.js 20.6 and later has a built-in `--env-file=.env` flag. When you run under systemd, systemd passes the file in itself - more on that below. If you need a database, installing it and setting up access is covered in the guide to [PostgreSQL on a VPS](https://tihost.io/en/blog/postgresql-on-vps).

## Step 5. Run the app under PM2

PM2 is a process manager for Node.js: it restarts a crashed app, collects logs and can reload processes one at a time. Install it globally as your sudo user:

```bash
sudo npm install -g pm2
```

Start the app, save the process list and enable startup at boot. `-i 2` turns on cluster mode with two processes - that is what makes zero-downtime updates possible, even on a 1 vCPU server. `pm2 startup` creates a `pm2-app` systemd service that restores the processes from `pm2 save` after a reboot:

```bash
sudo -iu app
cd ~/myapp
pm2 start server.js --name myapp -i 2
pm2 save
exit
sudo pm2 startup systemd -u app --hp /home/app
```

> Cluster mode suits apps that keep no state in process memory: store sessions and cache in Redis or the database, otherwise the two processes will see different data. If that is not your case, start without `-i 2` - everything else works the same. PM2 documentation lives at [pm2.keymetrics.io](https://pm2.keymetrics.io/).

## Alternative to PM2: a systemd service

If one process is enough, you can skip PM2: systemd ships with Ubuntu and Debian and does the same job - it starts the app at boot and restarts it 5 seconds after a crash. `EnvironmentFile` loads the variables from `.env` into the process environment, so dotenv is not needed. The downside is that `systemctl restart` stops the app while it restarts:

```bash
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF'
[Unit]
Description=Node.js app
After=network-online.target
Wants=network-online.target

[Service]
User=app
WorkingDirectory=/home/app/myapp
EnvironmentFile=/home/app/myapp/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
```

## Step 6. Check status and logs

`pm2 status` shows processes, their memory and restart count, and `pm2 logs` shows the app's output. The `pm2-logrotate` module rotates logs so they cannot fill the disk. With systemd, `systemctl status myapp` and `journalctl -u myapp -f` do the same. Run the PM2 commands as the `app` user:

```bash
pm2 status
pm2 logs myapp --lines 100
pm2 install pm2-logrotate
```

## Step 7. Put nginx in front of the app

A reverse proxy accepts requests from the internet and forwards them to the app on a local port. nginx handles HTTPS, compression, static files and rate limits, while port 3000 stays closed to the outside. The `Upgrade` and `Connection` headers are there for WebSocket. Save the config as `/etc/nginx/sites-available/myapp`, replacing `example.com` with your domain:

`/etc/nginx/sites-available/myapp`:

```
server {
    listen 80;
    server_name example.com;

    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";
    }
}
```

```bash
sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/myapp
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo ufw allow 80,443/tcp
```

The domain has to point at the server's IP - see the guide to [pointing a domain at a VPS](https://tihost.io/en/blog/domain-to-vps). A Let's Encrypt certificate and the HTTPS redirect take a couple of commands - follow the [nginx and Let's Encrypt](https://tihost.io/en/blog/nginx-letsencrypt-https) guide.

## Step 8. Update the app without downtime

In cluster mode `pm2 reload` starts a new process, waits for it to come up and only then stops the old one - one process at a time. Users see no errors while the new version rolls out:

```bash
sudo -iu app
cd ~/myapp
git pull
npm ci
npm run build --if-present
pm2 reload myapp
```

## What VPS does a Node.js app need?

An API, a Telegram bot or a small website on Node.js fits 1 vCPU and 2 GB of RAM - on Tihost that is the [Starter](https://tihost.io/en/blog/starter-vps) configuration for $4.00 a month. If a database and a frontend build share the same server, take 4 GB or more: the guide to [how much RAM a VPS needs](https://tihost.io/en/blog/how-much-ram-vps) walks through the math. Tihost servers run on KVM with NVMe and full root access and are ready about 2 minutes after payment.

**Launch a server in 2 minutes.** AMD Ryzen 9, NVMe and DDoS protection in Germany, Finland and Poland. Pay with crypto or card. [Order a Server](https://tihost.io/login)

## FAQ

### Which is better for Node.js on a server - PM2 or systemd?

PM2 is more convenient when you want cluster mode and zero-downtime updates via `pm2 reload`. systemd is built into Ubuntu and Debian and is simpler for a single process, but `systemctl restart` causes a short gap in service.

### How do I install Node.js LTS on Ubuntu?

Add the NodeSource repository with the `setup_lts.x` script and run `sudo apt install -y nodejs`. The alternative is nvm and `nvm install --lts` in a user's home directory.

### Why does my Node.js app stop when I close SSH?

An app started with `node server.js` from a terminal exits together with the SSH session. Run it under PM2 or as a systemd service - then it runs independently of SSH and comes back after a server reboot.

### Do I need nginx if Node.js can listen on a port by itself?

nginx in front of a Node.js app handles HTTPS, compression, static files and several sites on one IP, while the app's port stays closed to the outside. Without nginx you would have to let Node.js bind ports 80 and 443 and manage certificates in code.

### How do I update a Node.js app without downtime?

Run the app under PM2 in cluster mode (`pm2 start server.js -i 2`), and after `git pull` and `npm ci` run `pm2 reload myapp`: PM2 restarts the processes one by one while requests keep being served.

### How much memory does a Node.js app need on a VPS?

A small Node.js API or bot fits a VPS with 2 GB of RAM. If a database runs alongside it or the frontend is built on the server, 4-8 GB is more comfortable.

---

Updated 2026-10-05 · https://tihost.io/en/blog/nodejs-app-on-vps
