- Install Node.js on a server from the NodeSource repository or with nvm: the
nodejspackage 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 startlaunches the process,pm2 savestores the process list, andpm2 startupbrings 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 reloadrestarts Node.js processes one at a time, so a code update causes no downtime, whereaspm2 restartandsystemctl restartcause 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.
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:
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 -vThe 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.
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:
sudo adduser --disabled-password --gecos "" app
sudo -iu appStep 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):
git clone https://github.com/you/myapp.git ~/myapp
cd ~/myapp
npm ci
npm run build --if-presentFor 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.
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:
cat > ~/myapp/.env <<'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://myapp:password@localhost:5432/myapp
EOF
chmod 600 ~/myapp/.envThe 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.
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:
sudo npm install -g pm2Start 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:
sudo -iu app
cd ~/myapp
pm2 start server.js --name myapp -i 2
pm2 save
exit
sudo pm2 startup systemd -u app --hp /home/appAlternative 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:
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 myappStep 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:
pm2 status
pm2 logs myapp --lines 100
pm2 install pm2-logrotateStep 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:
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";
}
}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/tcpThe domain has to point at the server's IP - see the guide to pointing a domain at a VPS. A Let's Encrypt certificate and the HTTPS redirect take a couple of commands - follow the nginx and Let's Encrypt 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:
sudo -iu app
cd ~/myapp
git pull
npm ci
npm run build --if-present
pm2 reload myappWhat 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 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 walks through the math. Tihost servers run on KVM with NVMe and full root access and are ready about 2 minutes after payment.
AMD Ryzen 9, NVMe and DDoS protection in Germany, Finland and Poland. Pay with crypto or card.
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.