A VPS is a great place for Node.js apps such as Express APIs, Next.js sites, bots and real-time apps. The usual production setup has three parts: Node.js runs your code, PM2 keeps it running and restarts it after crashes or reboots, and Nginx receives visitors on ports 80/443 and forwards them to your app.
Which VPS is this for? These steps are for an Unmanaged VPS, where you log in as root and look after the server yourself. On a Managed VPS we take care of the operating system and server software, and you manage your websites in StackCP instead.
Step 1: Install Node.js (LTS)
Use a current Long Term Support release: Node.js 24 is the active LTS at the time of writing, and Node.js 22 is still supported. The NodeSource repository provides up-to-date packages.
Ubuntu and Debian
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo apt install -y nodejs node -v && npm -v
AlmaLinux and Rocky Linux
curl -fsSL https://rpm.nodesource.com/setup_24.x -o nodesource_setup.sh sudo bash nodesource_setup.sh sudo dnf install -y nodejs node -v && npm -v
Alternatively, nvm installs Node per user and makes switching versions easy.
Step 2: Put your app on the server
Run apps as your normal sudo user, never as root:
mkdir -p ~/apps && cd ~/apps git clone https://github.com/you/myapp.git cd myapp npm ci --omit=dev
Make sure the app listens on localhost and a port from the environment, for example in Express:
const port = process.env.PORT || 3000; app.listen(port, '127.0.0.1', () => console.log(`Listening on ${port}`));
Listening on 127.0.0.1 means only Nginx can reach it; visitors cannot bypass the proxy.
Step 3: Run it with PM2
sudo npm install -g pm2 nano ecosystem.config.js
Paste and adjust:
module.exports = { apps: [{ name: "myapp", script: "server.js", cwd: "/home/deploy/apps/myapp", instances: 1, max_memory_restart: "300M", env: { NODE_ENV: "production", PORT: 3000 } }] };
Start the app and check it:
pm2 start ecosystem.config.js pm2 status pm2 logs myapp
Start automatically after a reboot
pm2 startup systemd
PM2 prints a sudo env PATH=… pm2 startup systemd -u deploy --hp /home/deploy command. Copy and run it exactly, then save the current app list:
pm2 save
Test it with sudo reboot; after a minute, pm2 status should show the app online.
Rotate PM2's own log files so they don't fill the disk: pm2 install pm2-logrotate.
Step 4: Nginx reverse proxy
Install Nginx (sudo apt install nginx or sudo dnf install nginx then sudo systemctl enable --now nginx) and create a site file (/etc/nginx/sites-available/myapp on Ubuntu/Debian, /etc/nginx/conf.d/myapp.conf on AlmaLinux/Rocky):
server { listen 80; listen [::]:80; server_name app.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; } }
The Upgrade and Connection lines let WebSockets (for example Socket.IO) work. Enable and reload:
# Ubuntu / Debian only sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ # all systems sudo nginx -t && sudo systemctl reload nginx # AlmaLinux / Rocky only: allow Nginx to connect to your app sudo setsebool -P httpd_can_network_connect 1
Point app.example.com to the VPS (DNS guide), open ports 80 and 443 in your firewall, then add HTTPS with sudo certbot --nginx -d app.example.com (Certbot guide).
In Express, add app.set('trust proxy', 1) so the app sees the visitor's real IP and HTTPS status.
Deploying updates
cd ~/apps/myapp git pull npm ci --omit=dev npm run build --if-present pm2 reload myapp
pm2 reload restarts with minimal downtime.
Common problems
- 502 Bad Gateway: the app isn't running or uses a different port. Check
pm2 status,pm2 logsandss -tlnp | grep 3000. - EADDRINUSE: another process already uses the port. Find it with
sudo ss -tlnp | grep :3000. - App is gone after a reboot: run the
pm2 startupcommand it printed, thenpm2 save. - App keeps restarting: it's crashing or hitting
max_memory_restart. Readpm2 logs myapp --lines 100; add swap or upgrade the plan if memory is short. - WebSockets fail: the
Upgrade/Connectionheaders are missing from the Nginx block.
Need help?
If something about the VPS itself is not working (it won't start, you can't reach it, or you need console access, an upgrade or a reinstall), open a support ticket from your client area or message us on WhatsApp at 01818160926. Include your VPS IP address and what you have already tried so we can help faster.
Categories
Written by
FimuroHost Team
Technical Writer