How to Deploy a Node.js App on a VPS

Step-by-step guide to deploy a Node.js app on a VPS with Nginx, PM2, SSL, and firewall configuration.

Disclosure: This post contains affiliate links. If you purchase a service through one of these links, I may earn a commission at no extra cost to you. I only recommend products and services I have personally used or thoroughly evaluated. Your support helps keep HostBeacons running — thank you.

How to Deploy a Node.js App on a VPS

So you have built a Node.js application, it runs beautifully on localhost, and now you need to put it somewhere the rest of the world can actually reach it. Shared hosting is not going to cut it. Platform-as-a-service options like Heroku or Render are convenient, but they get expensive fast once you need real resources, and they abstract away things you might actually want to control. A Virtual Private Server is the sweet spot — full root access, predictable pricing, and enough flexibility to host one project or twenty.

I have deployed dozens of Node.js apps to VPS instances over the years, and the process has gotten remarkably smooth. In this guide, I will walk you through every step: choosing a provider, logging in via SSH, installing Node.js the right way, getting your app running with a process manager, putting Nginx in front of it, securing everything with a free SSL certificate, and locking the server down with a firewall. By the end, you will have a production-ready setup that can handle real traffic.

If you are not sure what a VPS actually is or why it matters, I recommend reading our guide on what VPS hosting is before continuing. It will give you the foundational context that makes everything here click.

Step 1: Choose a VPS Provider

The provider you pick matters less than you think, as long as they offer a reliable network, KVM-based virtualization, and a straightforward control panel. That said, pricing and support quality do vary, and you do not want to discover problems after you have migrated your production app.

I have been using InterServer for several of my own projects, and it is the provider I recommend for Node.js deployments. Their standard VPS plan starts at $6 per month, gives you one CPU core, 2 GB of RAM, 30 GB of SSD storage, and unmetered bandwidth on a 1 Gbps port. That is more than enough to run a moderately trafficked Node.js application, a reverse proxy, and a database if you need one. What I appreciate most about InterServer is their price-lock guarantee — whatever you pay when you sign up is what you pay for life. No bait-and-switch renewal pricing.

They also offer instant provisioning with a choice of operating systems (I recommend Ubuntu 22.04 LTS or Ubuntu 24.04 LTS for this tutorial), a clean web-based console, and 24/7 support from humans who actually know what a reverse proxy is. If you are looking for more options, check out our roundup of the best VPS providers under $10 per month.

For this tutorial, I will assume you have just provisioned a fresh Ubuntu server. You should have received an IP address, a root username, and a password (or SSH key) from your provider.

Step 2: Connect to Your Server via SSH

Open your terminal. On macOS and Linux, SSH is built in. On Windows, you can use Windows Terminal, PowerShell, or an application like PuTTY. Connect to your server with the following command, replacing the IP address with your own:

ssh [email protected]

If this is your first connection, you will be asked to confirm the server fingerprint. Type “yes” and enter your password when prompted.

Once you are logged in, it is good practice to create a non-root user for everyday tasks. Running everything as root is a security risk you do not need to take.

adduser deploy
usermod -aG sudo deploy

Now set up SSH key authentication for the new user. On your local machine, generate a key pair if you do not have one already:

ssh-keygen -t ed25519 -C "[email protected]"

Copy the public key to your server:

ssh-copy-id [email protected]

From this point forward, log in as your deploy user:

ssh [email protected]

First things first — update the system:

sudo apt update && sudo apt upgrade -y

Step 3: Install Node.js Using NVM

Do not install Node.js from Ubuntu’s default repositories. The versions there are almost always outdated. Instead, use NVM (Node Version Manager), which lets you install and switch between any Node.js version with a single command. This is how professional Node.js developers manage their runtimes, and it works just as well on a server as it does on a development machine.

Install NVM:

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

Reload your shell configuration so the nvm command becomes available:

source ~/.bashrc

Now install the latest LTS version of Node.js:

nvm install --lts

Verify the installation:

node --version
npm --version

You should see something like v22.x.x for Node and 10.x.x for npm (the exact numbers depend on when you run this). NVM also lets you install specific versions if your project requires one. For example, nvm install 20 would give you the latest Node.js 20.x release.

Step 4: Clone Your Application Repository

If your project is hosted on GitHub, GitLab, or Bitbucket, cloning it to the server is straightforward. First, install Git if it is not already present:

sudo apt install git -y

Create a directory for your application and clone the repository:

mkdir -p /home/deploy/apps
cd /home/deploy/apps
git clone https://github.com/yourusername/your-nodejs-app.git
cd your-nodejs-app

If your repository is private, you will need to set up a deploy key or use a personal access token. For GitHub, the deploy key approach is cleaner — generate a key pair on the server, add the public key as a deploy key in your repository settings, and clone via SSH instead of HTTPS:

git clone [email protected]:yourusername/your-nodejs-app.git

Step 5: Install Dependencies

Navigate into your project directory and install the production dependencies:

cd /home/deploy/apps/your-nodejs-app
npm install --production

The --production flag tells npm to skip devDependencies, which you do not need on the server. This keeps the installation lighter and faster.

If your application uses environment variables (and it should — never hardcode credentials), create a .env file:

nano .env

Add your variables:

NODE_ENV=production
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
SESSION_SECRET=your_random_secret_here

Make sure your application reads from process.env correctly, and that .env is listed in your .gitignore so it never ends up in your repository.

Test that your application starts without errors:

node app.js

If you see your application’s startup message and no errors, you are in good shape. Press Ctrl+C to stop it — we are going to set up a proper process manager next.

Step 6: Set Up PM2 as a Process Manager

Running your app with node app.js directly is fine for testing, but it is not a production strategy. If the process crashes, nobody restarts it. If the server reboots, your app stays down. PM2 solves all of this. It is a production-grade process manager for Node.js that handles automatic restarts, log management, clustering, and startup scripts.

Install PM2 globally:

npm install -g pm2

Start your application with PM2:

cd /home/deploy/apps/your-nodejs-app
pm2 start app.js --name "my-app"

If your application’s entry point is different (like server.js or index.js), adjust accordingly. You can also pass environment variables directly:

pm2 start app.js --name "my-app" --env production

Check that your app is running:

pm2 status

You should see a table listing your app with a status of “online.” Some other useful PM2 commands you will want to know:

pm2 logs my-app        # View real-time logs
pm2 restart my-app     # Restart the app
pm2 stop my-app        # Stop the app
pm2 delete my-app      # Remove from PM2's process list
pm2 monit              # Real-time monitoring dashboard

Now, the critical part — tell PM2 to start your app automatically when the server boots:

pm2 startup systemd

PM2 will output a command that you need to copy and run with sudo. It will look something like this:

sudo env PATH=$PATH:/home/deploy/.nvm/versions/node/v22.x.x/bin pm2 startup systemd -u deploy --hp /home/deploy

Run that exact command, then save the current process list so PM2 knows what to start on boot:

pm2 save

Your Node.js app is now running as a managed service that will survive crashes and reboots. But it is listening on port 3000 (or whatever port you configured), and you do not want to expose that directly to the internet. That is where Nginx comes in.

Step 7: Configure Nginx as a Reverse Proxy

Nginx will sit in front of your Node.js application, accept HTTP and HTTPS traffic on ports 80 and 443, and forward requests to your app on port 3000. This gives you several benefits: you can serve static assets more efficiently, handle SSL termination, add rate limiting, and run multiple apps on the same server with different domain names.

Install Nginx:

sudo apt install nginx -y

Create a new configuration file for your site:

sudo nano /etc/nginx/sites-available/your-nodejs-app

Add the following configuration, replacing yourdomain.com with your actual domain name:

server {
    listen 80;
    server_name yourdomain.com www.yourdomain.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;
        proxy_cache_bypass $http_upgrade;
    }
}

The Upgrade and Connection headers are important if your app uses WebSockets. The X-Real-IP and X-Forwarded-For headers ensure your application sees the client’s actual IP address rather than 127.0.0.1.

Enable the site by creating a symbolic link and remove the default configuration:

sudo ln -s /etc/nginx/sites-available/your-nodejs-app /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default

Test the Nginx configuration for syntax errors:

sudo nginx -t

If you see “syntax is ok” and “test is successful,” restart Nginx:

sudo systemctl restart nginx

Make sure your domain’s DNS A record points to your server’s IP address. Once DNS has propagated, visiting http://yourdomain.com in a browser should show your Node.js application. We still need HTTPS though — let us fix that next.

Step 8: Set Up SSL with Let’s Encrypt

There is no excuse to run a production website without HTTPS in 2026. Let’s Encrypt provides free, automated SSL certificates, and Certbot makes the installation trivial.

Install Certbot and its Nginx plugin:

sudo apt install certbot python3-certbot-nginx -y

Run Certbot and let it configure Nginx automatically:

sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com

Certbot will ask for your email address (for renewal notifications), ask you to agree to the terms of service, and then automatically obtain the certificate and modify your Nginx configuration to use it. It will also set up a redirect from HTTP to HTTPS.

Verify that automatic renewal is working:

sudo certbot renew --dry-run

If the dry run succeeds, Certbot will automatically renew your certificates before they expire. The systemd timer that handles this is installed automatically. You can check it with:

sudo systemctl status certbot.timer

Visit https://yourdomain.com now and you should see the padlock icon in your browser. Your Node.js app is now served over a secure connection. For a deeper dive into keeping everything locked down, read our guide on how to secure your website.

Step 9: Set Up a Firewall with UFW

Your server is now serving your app, but every port on the machine is still wide open to the internet by default. That is an unnecessary risk. UFW (Uncomplicated Firewall) is Ubuntu’s front end for iptables, and it does exactly what its name suggests — makes firewall management uncomplicated.

Start by checking the current status:

sudo ufw status

It is likely inactive. Before enabling it, you must allow SSH access, or you will lock yourself out of the server:

sudo ufw allow OpenSSH

Allow HTTP and HTTPS traffic for Nginx:

sudo ufw allow 'Nginx Full'

Now enable the firewall:

sudo ufw enable

Confirm the rules are active:

sudo ufw status verbose

You should see something like this:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
Nginx Full                 ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
Nginx Full (v6)            ALLOW       Anywhere (v6)

Notice that port 3000 is not listed. That is intentional. Your Node.js app is only accessible through Nginx on ports 80 and 443, not directly. This is exactly how it should be.

If you run other services (like a database that needs external access, or a custom application on another port), you can allow specific ports as needed:

sudo ufw allow 27017    # Example: MongoDB (only if you truly need external access)

But in most cases, keep your firewall rules as restrictive as possible. Only open what you need.

Deploying Updates

Once everything is set up, deploying updates to your application is a simple sequence of commands:

cd /home/deploy/apps/your-nodejs-app
git pull origin main
npm install --production
pm2 restart my-app

For zero-downtime deployments, you can use PM2’s reload command instead of restart:

pm2 reload my-app

This gracefully restarts the process, waiting for existing connections to close before spinning up the new version. If you are deploying frequently, consider writing a simple shell script or setting up a CI/CD pipeline with GitHub Actions or GitLab CI to automate this process.

Frequently Asked Questions

How much RAM does a Node.js app need on a VPS?

For a typical Express or Fastify application, 1 GB of RAM is a workable minimum, but 2 GB gives you much more breathing room once you factor in the operating system, Nginx, PM2, and any background processes. If you are running a database on the same server, aim for 4 GB. InterServer’s VPS plans start at 2 GB and scale easily when you need more.

Can I run multiple Node.js apps on one VPS?

Absolutely. Run each app on a different port (3000, 3001, 3002, and so on), manage them all with PM2, and create separate Nginx server blocks for each domain or subdomain. A 2 GB VPS can comfortably handle two or three lightweight Node.js applications.

Should I use Nginx or Apache as a reverse proxy?

Nginx. It is significantly faster at handling concurrent connections, uses less memory, and its configuration for reverse proxying is cleaner. Apache can do the job, but there is no compelling reason to choose it over Nginx for a Node.js reverse proxy in 2026.

Do I need PM2, or can I just use systemd?

You can use systemd directly by writing a service unit file, and some people prefer that approach for its simplicity. However, PM2 gives you automatic clustering, built-in log rotation, a monitoring dashboard, and seamless restart capabilities that you would otherwise have to configure manually. For most Node.js developers, PM2 is the more productive choice.

How do I handle environment variables securely?

Store them in a .env file on the server with restricted permissions (chmod 600 .env), and make sure .env is in your .gitignore. Never commit secrets to your repository. For more sensitive deployments, consider using a secrets manager or environment variable injection through your CI/CD pipeline.

What if my app crashes and PM2 keeps restarting it?

PM2 will attempt to restart a crashed application by default, but if it crashes repeatedly in quick succession, PM2 will stop trying and mark the process as “errored.” Check the logs with pm2 logs my-app to diagnose the issue. Common culprits include missing environment variables, database connection failures, and port conflicts.

Is a $6/month VPS really enough for production?

For a small to medium-traffic application, yes. A well-optimized Node.js app can handle thousands of concurrent connections on modest hardware. The key is to monitor your resource usage with tools like htop and PM2’s monitoring, and upgrade when you see CPU or memory consistently above 80 percent. The beauty of a VPS is that scaling up usually just means clicking a button and choosing a larger plan.

How do I set up a custom SSH port for extra security?

Edit /etc/ssh/sshd_config, change the Port directive to your chosen number (e.g., 2222), allow that port in UFW (sudo ufw allow 2222/tcp), and restart the SSH service. Remember to update your firewall rules before restarting SSH, or you will lock yourself out.

Wrapping Up

You now have a Node.js application running in production on a VPS with a process manager that handles restarts, a reverse proxy that manages traffic and SSL termination, a free HTTPS certificate that renews itself, and a firewall that only allows the traffic you want. This is a solid, battle-tested stack that will serve you well whether you are running a personal project, a client site, or a growing SaaS application.

If you have not set up your server yet, I recommend starting with InterServer’s VPS hosting. Their combination of fair pricing, reliable infrastructure, and genuine technical support makes them my go-to recommendation for developers who want to focus on building rather than fighting with their hosting provider. And with their price-lock guarantee, you will never have to deal with surprise renewal increases.

The setup process in this guide takes about 30 minutes once you have done it a couple of times. After that, deploying updates is a matter of seconds. That is the real payoff of a VPS — once the infrastructure is in place, it just works, and you have complete control over every layer of the stack.

Leave a Reply

Your email address will not be published. Required fields are marked *