Running Node.js in production on a VPS needs four things: a supported Node version, a process manager that restarts your app, a reverse proxy for HTTPS, and a repeatable way to deploy. Here’s a clean setup on Ubuntu or Debian.

1. Install Node.js LTS

Use an LTS (long-term support) release. Two good options:

NodeSource packages (system-wide, updates with apt):

curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
node -v && npm -v

nvm (per-user, easy to switch versions):

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install --lts

For services managed by systemd, a system-wide install is simpler. Check the Node.js release schedule and move to a newer LTS before yours reaches end of life.

2. Create an app user and deploy the code

adduser --system --group --home /srv/myapp myapp
git clone https://github.com/you/myapp.git /srv/myapp/current
cd /srv/myapp/current
npm ci --omit=dev
chown -R myapp: /srv/myapp

Store configuration in an environment file, not in Git:

cat > /srv/myapp/.env <<'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgres://…
EOF
chmod 600 /srv/myapp/.env && chown myapp: /srv/myapp/.env

Make sure the app listens on 127.0.0.1, not 0.0.0.0, so only Nginx can reach it.

3. Run it with systemd

/etc/systemd/system/myapp.service:

[Unit]
Description=My Node.js app
After=network.target

[Service]
Type=simple
User=myapp
WorkingDirectory=/srv/myapp/current
EnvironmentFile=/srv/myapp/.env
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=3
LimitNOFILE=65535

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/myapp

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now myapp
journalctl -u myapp -f

Or use PM2

PM2 adds clustering across CPU cores, log management and zero-downtime reloads:

npm install -g pm2
sudo -u myapp pm2 start server.js --name myapp -i max
sudo -u myapp pm2 save
pm2 startup systemd -u myapp --hp /srv/myapp

Both approaches are fine; systemd has fewer moving parts, PM2 is handy for clustering.

4. Nginx reverse proxy with HTTPS

server {
    server_name app.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";
    }
}
nginx -t && systemctl reload nginx
certbot --nginx -d app.example.com --redirect

In Express, enable app.set('trust proxy', 1) so req.ip and secure cookies work behind Nginx. WebSocket details: secure WebSocket server.

5. Deploy updates

A simple, safe deploy script:

#!/bin/bash
set -euo pipefail
cd /srv/myapp/current
sudo -u myapp git pull --ff-only
sudo -u myapp npm ci --omit=dev
sudo -u myapp npm run build --if-present
systemctl restart myapp       # or: sudo -u myapp pm2 reload myapp

For automated deploys from GitHub, see full-stack deployment on a VPS.

6. Production checklist

  • NODE_ENV=production
  • App bound to localhost; firewall allows only 22/80/443
  • Logs: journalctl -u myapp (systemd keeps them rotated) or PM2’s log rotation module
  • Health endpoint (/health) for uptime monitoring
  • npm audit and dependency updates on a schedule
  • Memory limits: watch RSS; node --max-old-space-size=… for large heaps

Sizing

A small API or bot runs on 1 GB RAM; typical web apps with a database are comfortable on 2–4 GB. Our Ubuntu VPS and Docker VPS pages list suitable plans. If you’d rather containerize, see Nginx reverse proxy for Docker containers.

  • Node.js
  • Nginx
  • Deployment