How I Deployed a Node.js App on a Cheap VPS with Nginx, HTTPS, and PM2 (Part 1: Server Setup)

Deploy a Node.js App on a Cheap VPS: Server Setup (Part 1) | SpiritCode” meta_description:

Managed hosting is convenient, but the bill adds up fast once you have a few small projects. I moved one of my Node.js apps to a low-cost VPS, and I want to show you exactly how I did it, step by step.

This is Part 1 of 2. Here I set up a secure Ubuntu server, install Node.js, and keep the app alive with PM2. In Part 2, I put Nginx in front of it, connect a domain, and add free HTTPS.

What a Cheap VPS Really Costs

Entry-level VPS plans from providers like DigitalOcean, Vultr, Linode (Akamai), and Hetzner typically start in the single-digit dollars per month. Prices, specs, and data center locations change often, so check each provider’s current pricing page before you buy.

For a small Node.js app, here is the minimum I’d look for:

  • 1 vCPU
  • 1 GB of RAM (2 GB is more comfortable)
  • 20+ GB SSD storage
  • A data center in the US (East or West, depending on where your users are)
  • Ubuntu 24.04 LTS or 22.04 LTS as the operating system

Remember to budget for a domain name too. That is usually a separate yearly cost.

Step 1: Create the Server and Log In

When I create the VPS, I choose Ubuntu LTS, add my SSH key (recommended over a password), and pick a US region. The provider gives me an IP address, and I connect:

ssh root@YOUR_SERVER_IP

If you don’t have an SSH key yet, create one on your computer with ssh-keygen -t ed25519 and paste the contents of the .pub file into your provider’s dashboard.

Step 2: Update the System

apt update && apt upgrade -y

Step 3: Create a Non-Root User

I never run my app as root. If something goes wrong, root access makes the damage much worse.

adduser deploy
usermod -aG sudo deploy

Copy your SSH key to the new user so you can log in as deploy:

rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy

Open a new terminal and test the login before closing your root session:

ssh deploy@YOUR_SERVER_IP

From here on, I use this user.

Step 4: Turn On the Firewall

Ubuntu includes ufw, a simple firewall. I allow SSH first so I don’t lock myself out:

sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status

In Part 2, I’ll open the web ports after installing Nginx.

Step 5: Install Node.js

I use the NodeSource repository so I get a current LTS version. Check nodejs.org for the current LTS number and replace 22.x if a newer one is available:

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

Step 6: Get Your App onto the Server

I keep my code in a Git repository, so I clone it:

cd ~
git clone https://github.com/YOUR_USER/YOUR_REPO.git myapp
cd myapp
npm ci --omit=dev

If your project needs a build step (TypeScript, Next.js, and so on), run it now:

npm run build

For private repositories, I add a deploy key on GitHub instead of using my personal password.

Environment variables

I never commit secrets. I create a .env file on the server:

nano .env
PORT=3000
NODE_ENV=production
DATABASE_URL=your-connection-string

Make sure your app listens on process.env.PORT and, ideally, only on 127.0.0.1 since Nginx will sit in front of it. A quick test run:

node server.js

If it starts without errors, stop it with Ctrl + C.

Step 7: Keep the App Running with PM2

If I just run node server.js, the app dies when I close my terminal or when it crashes. PM2 is a process manager that fixes both problems.

sudo npm install -g pm2
pm2 start server.js --name myapp
pm2 status

Now I make sure PM2 restarts the app after a server reboot:

pm2 startup

PM2 prints a command that starts with sudo env PATH=.... Copy that exact command and run it. Then save the current process list:

pm2 save

Useful commands I use all the time:

pm2 logs myapp      # view live logs
pm2 restart myapp   # restart after an update
pm2 stop myapp      # stop the app

If your app loads the .env file with a library like dotenv, PM2 will pick it up from the project folder. If it doesn’t, you can pass variables through a PM2 ecosystem file.

Quick Check

Before moving on, I confirm the app responds locally on the server:

curl http://127.0.0.1:3000

If I see my app’s response, Part 1 is done. The app runs, restarts automatically, and is not yet exposed to the internet, which is exactly what I want at this stage.

What’s Next

In Part 2, I install Nginx as a reverse proxy, point a domain name to the server, add free HTTPS with Let’s Encrypt, and fix the dreaded 502 Bad Gateway error.

Continue to: How I Deployed a Node.js App on a Cheap VPS with Nginx, HTTPS, and PM2 (Part 2: Nginx and HTTPS).