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

