If you've been developing locally and it's time to put a Node.js app in front of real users, a small VPS (DigitalOcean, Linode, Hetzner) paired with PM2 and Ng…
If you've been developing locally and it's time to put a Node.js app in front of real users, a small VPS (DigitalOcean, Linode, Hetzner) paired with PM2 and Nginx is one of the most reliable, low-cost setups out there — no platform lock-in, full control, and it costs a few dollars a month. Here's the full path from a fresh server to a live HTTPS site.
Why This Stack
- PM2 keeps your Node process alive, restarts it on crashes, and survives server reboots
- Nginx sits in front as a reverse proxy — handles incoming traffic, SSL termination, and can serve static files directly
- Let's Encrypt gives you free, auto-renewing HTTPS certificates
This combo is what a huge share of production Node apps run on, especially for solo devs who don't want the overhead of a managed platform.
Step 1: Provision and Secure the Server
Spin up an Ubuntu 22.04 (or 24.04) droplet/VPS. Once you SSH in:
apt update && apt upgrade -y
adduser deploy
usermod -aG sudo deploy
Switch to that user for everything from here on — running your app as root is asking for trouble.
Set up a basic firewall:
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
Step 2: Install Node.js and PM2
Use NodeSource to get a current LTS version rather than the outdated one in Ubuntu's default repos:
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs
npm install -g pm2
Verify:
node -v
npm -v
pm2 -v
Step 3: Deploy Your App
Clone your repo (or scp your files over) into something like /var/www/myapp:
cd /var/www
git clone https://github.com/yourname/myapp.git
cd myapp
npm install --production
Create a .env file with your production secrets, then start the app under PM2:
pm2 start server.js --name myapp
pm2 save
pm2 startup
That last command prints a sudo line — run it, and PM2 will now restart your app automatically on server reboot.
Useful PM2 commands going forward:
pm2 list # see running processes
pm2 logs myapp # tail logs
pm2 restart myapp # after deploying new code
Step 4: Configure Nginx as a Reverse Proxy
Install Nginx:
sudo apt install -y nginx
Create a site config at /etc/nginx/sites-available/myapp:
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Enable it and reload:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Tip: nginx -t tests your config before reloading — always run it first. A typo here can take down every site on the box.
Step 5: Add Free HTTPS with Certbot
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot edits your Nginx config automatically to handle SSL and sets up a redirect from HTTP to HTTPS. It also installs a renewal cron job/timer — certificates renew silently every ~60 days.
Test renewal without waiting:
sudo certbot renew --dry-run
Common Pitfalls
- Point your DNS first. The A record for your domain must resolve to the server's IP before running Certbot, or the domain validation will fail.
- Port mismatch. Make sure the port in your Nginx
proxy_pass matches whatever your app actually listens on.
- Forgetting
pm2 save after changes. If you start a new process and don't save, it won't come back after a reboot.
- Env vars not loading. If you're using a
.env file, make sure something like dotenv is actually required at the top of your entry file — PM2 won't source shell env files for you.
Wrapping Up
Once this is set up, deploying updates is just git pull && npm install && pm2 restart myapp — a rhythm that becomes second nature fast. It's not the flashiest deployment pipeline, but for a solo developer shipping utility tools or side projects, it's dependable, cheap, and fully within your control.