How to Deploy a Node.js App on a VPS

2026-05-05
How to Deploy a Node.js App on a VPS Featured Image

Deploying a Node.js application on a VPS (Virtual Private Server) gives you full control over performance, security, and scalability. With Maavee Systems, you also get built-in infrastructure advantages like a perimeter firewall, optional static IPs, and managed SSL via Maavee Load Balancer.

This guide shows both:

Standard deployment
Optimized deployment using Maavee infrastructure

Maavee Load Balancer is a specially configured software load balancer designed to maintain high-speed, reliable traffic. It protects you from complex configuration issues and ensures that whatever you build here remains portable across any cloud provider in the world.

What You Need Before Starting

A Maavee VPS
SSH access
A Node.js application
A domain name (optional but recommended)

Connect to and Update Your VPS Server

ssh root@your-server-ip
sudo apt update && sudo apt upgrade -y

Create a Sample Application

const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware to parse JSON bodies
app.use(express.json());

// 1. A basic GET route for the homepage
app.get('/', (req, res) => {
res.send('<h1>Welcome to the Node.js Web App</h1><p>The server is running smoothly.</p>');
});

// 2. A JSON API route
app.get('/api/status', (req, res) => {
res.json({
status: 'online',
timestamp: new Date().toISOString(),
uptime: process.uptime()
});
});

// 3. A POST route to receive data
app.post('/api/data', (req, res) => {
const receivedData = req.body;

if (!receivedData.name) {
return res.status(400).json({ error: 'Please provide a name.' });
}

res.status(201).json({
message: 'Data received successfully',
yourData: receivedData
});
});

// 4. Handle 404 - Keep this at the end of your routes
app.use((req, res) => {
res.status(404).send('Resource Not Found');
});

// Start the server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server is running at http://localhost:${PORT}`);
});

This is the main entry point for your application. app.js

Code Deployment: A Production Perspective

At Maavee, we encourage the use of Forgejo, but you can choose how your code is delivered to the server. A Forgejo runner agent active on the target server is often the ideal solution.

Create a separate user for running the Node.js application. Ensure it is configured with a non-login shell.

groupadd runner
useradd -g runner -m -d /opt/runner -s /usr/sbin/nologin runner

Switch to the new user by specifying a login shell:

su - runner -s /bin/bash

Install Node.js within the user account; avoid installing it globally system-wide.

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash

Reload your shell (or log out and log back in via SSH) to load nvm.

nvm install 24

At the time of writing, Node.js version 24 is the latest stable LTS version.

Install PM2 globally within the user account:

npm install -g pm2

Configure the Forgejo runner (you can use the screen utility or a systemd unit file to keep the runner active). Forgejo Config file

forgejo-runner daemon -c $HOME/forgejo-config.yml

A Forgejo Actions workflow can pull the code to the relevant directory and instruct PM2 to stop or restart the service. It's easy for anyone to find information on how to write a Forgejo actions workflow.

name: Deploy Nodejs Application

on:
push:
branches:
- 'master'

jobs:
build-and-test:
runs-on: nodejs-tute

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
lfs: true

- name: Show insight
run: |
ls -l
pwd

- name: Copy content to user
run: |
mkdir -p ${HOME}/nodejs-tutes
rsync -avz --delete \
--exclude '.git*' \
./ ${HOME}/nodejs-tutes/

- name: Install npm packages
run: |
cd ${HOME}/nodejs-tutes
npm install

- name: Restart service
run: |
cd ${HOME}/nodejs-tutes
pm2 restart all

Deployment Architecture Options (Maavee-Specific)

Option A: Direct VPS Deployment (Simplest)

Use this if:

You have a static IP.
You want full control within your VPS setup.
You prefer to install Nginx and Certbot directly on your VPS.

Option B: Maavee Load Balancer + Managed SSL (Recommended)

Use this if:

You want automatic SSL.
You prefer not to manage certificates yourself.
You are using Maavee's Load Balancer.

How it Works

Your app runs on: 0.0.0.0:3000
MLB handles:
SSL termination (Managed via Certbot)
Public traffic routing
Traffic is forwarded securely to your VPS.

Important Requirement

To use this setup effectively:

Your VPS should have a static IP
Alternatively, it must be on a routable internal network accessible by the MLB.

The Firewall

Maavee provides multi-layered protection:

Perimeter Firewall (External to the VPS)

Protects your VPS at the network level.
Blocks unwanted traffic before it reaches your server.

Internal Firewall (iptables / UFW)

You control the rules inside the VPS:

pm2 start app.js
pm2 startup
pm2 save

Domain and DNS Configuration

To make your application accessible via a custom domain, you have two primary paths depending on your chosen architecture:

For Direct VPS Deployment

Point your domain's A Record directly to your VPS’s public IP address. If using Nginx, you will then manage SSL certificates locally using Certbot.

For Maavee Load Balancer (MLB) Setup

Point your domain's CNAME Record to loadb.maavee.com. The Maavee Load Balancer will automatically handle SSL termination and route traffic to your VPS. This ensures your application remains resilient even if your underlying VPS IP changes.

Security Hardening

Beyond the firewall, we recommend these essential steps to secure your production environment:

SSH Key Authentication

Disable password-based logins and use SSH keys for all access.

Copy your public key to the server: ssh-copy-id runner@your-server-ip
Edit /etc/ssh/sshd_config:
Set PasswordAuthentication no
Set PermitRootLogin no
Restart SSH: sudo systemctl restart ssh

Environment Isolation

Always run your application under the dedicated runner user created earlier. Never run Node.js processes as root.

Advanced Monitoring and Persistence

Automatic Restarts on Reboot

To ensure your application recovers automatically after a server restart, use PM2's startup script generator:

pm2 startup
# Run the command generated by the output above
pm2 save

Log Management

Production logs can quickly consume disk space. We recommend installing pm2-logrotate:

pm2 install pm2-logrotate

Real-time Oversight

Monitor your application's resource usage and health in real-time:

pm2 monit

Final Thoughts

Maavee Systems provides flexibility at every layer of your stack:

Infrastructure-level security: Our Perimeter Firewall filters threats before they ever reach your virtual network.
Application-level control: Full sovereignty over your environment within your VPS.

For most production workloads, the MLB + Managed SSL provides security, simplicity, and high-speed traffic handling. By offloading SSL and load balancing to Maavee's specialized infrastructure, you can focus entirely on building your application.