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 auditand 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.