From Localhost Hero to Production Legend
So your app works perfectly on your laptop. You run npm start, you visit localhost:3000, and everything is beautiful. Then you deploy it to a server, close your terminal, and... it dies. Or worse, it survives until 3 AM and crashes right when your first real user shows up. Sound familiar? Let's fix that forever.
In this guide, we'll turn a lonely Node.js process into a bulletproof production setup using two of the best friends a backend dev can have: PM2 and Nginx. Think of PM2 as the babysitter that keeps your app alive, and Nginx as the friendly bouncer that greets visitors at the front door.
Meet the Cast
Before we start flipping switches, let's get to know our two heroes so you actually understand what you're doing instead of blindly copy-pasting commands (though we won't judge if you do).
PM2: The Process Babysitter
PM2 is a production process manager for Node.js. Its whole job is to keep your app running no matter what. App crashed? PM2 restarts it. Server rebooted? PM2 brings your app back. Want to run your app across all CPU cores? PM2 does that too. It's basically a very responsible adult watching over your app so you can sleep at night.
Nginx: The Friendly Bouncer
Nginx is a blazing-fast web server that we'll use as a reverse proxy. Instead of exposing your Node app directly on some weird port, Nginx sits at the front door on ports 80 and 443, greets every visitor, and quietly forwards their requests to your app running in the background. It also handles HTTPS, compression, and serving static files like a champ.
Step 1: Get Your App Ready
First things first, make sure your app actually reads its port from an environment variable. Hardcoding port 3000 everywhere is a rookie move that will haunt you. Here's the pattern you want:
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
That tiny change means Nginx and PM2 can tell your app which port to use later, and future-you will be very grateful.
Step 2: Hire PM2
Install PM2 globally on your server so it's available everywhere:
npm install -g pm2
Now start your app under PM2's watchful eye. Give it a friendly name so you can find it later:
pm2 start app.js --name my-api
And just like that, your app is running in the background. Close your terminal, log out, do a little dance. It keeps running. Here are the commands you'll use every day:
pm2 list # see all your running apps
pm2 logs my-api # tail the logs in real time
pm2 restart my-api
pm2 stop my-api
pm2 delete my-api
Step 3: Survive a Reboot
Here's the part everyone forgets until their server reboots at 2 AM and the app doesn't come back. Tell PM2 to start on boot and save your current process list:
pm2 startup # prints a command, copy and run it
pm2 save # remembers your currently running apps
Now your app is officially immortal. Reboots, crashes, cosmic rays: PM2 has your back.
Step 4: Bring in Nginx
Your app is happily running on port 3000, but nobody wants to type yoursite.com:3000 into their browser. This is where Nginx becomes the polished receptionist standing at the front door on port 80. Install it:
sudo apt update && sudo apt install nginx
Now create a config for your site. Open a new file at /etc/nginx/sites-available/my-api and drop in this reverse proxy setup:
server {
listen 80;
server_name yoursite.com www.yoursite.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 the site by symlinking it, test that your config isn't broken, and reload Nginx:
sudo ln -s /etc/nginx/sites-available/my-api /etc/nginx/sites-enabled/
sudo nginx -t # test the config, always do this!
sudo systemctl reload nginx
Visit yoursite.com and there it is: your app, no scary port number in sight. Nginx quietly forwards every request to your Node app on port 3000, and your visitors are none the wiser.
Step 5: Add HTTPS Because It's Not 2005
A production site without HTTPS is like a bank vault with a screen door. Luckily, Let's Encrypt gives you free SSL certificates and Certbot makes setup almost embarrassingly easy:
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yoursite.com -d www.yoursite.com
Certbot automatically edits your Nginx config to handle HTTPS and even sets up auto-renewal, so your certificate never expires while you're on vacation. That little padlock in the browser? You earned it.
Bonus: Use All Your CPU Cores
Your server probably has multiple CPU cores, but a single Node process only uses one of them. PM2's cluster mode spins up one instance per core and load-balances between them, for free:
pm2 start app.js --name my-api -i max
The -i max flag tells PM2 to use every available core. More throughput, better resilience, zero extra code.
The Recap
Let's zoom out and appreciate what you just built. Here's who's doing what in your shiny new production setup:
PM2 keeps your Node app alive, restarts it on crashes, survives reboots, and can run across every CPU core.
Nginx sits at the front door, forwards requests to your app, and serves everything on clean ports 80 and 443.
Certbot + Let's Encrypt wraps everything in free, auto-renewing HTTPS.
Together they turn a fragile
npm startinto a setup that shrugs off crashes, reboots, and traffic spikes.
Go Deploy Something
That's it! You've officially graduated from localhost hero to production legend. The next time your app runs for weeks without you touching it, quietly serving real users over HTTPS while you sleep, you'll know exactly who to thank: your two loyal friends, PM2 and Nginx. Now go break things in production... responsibly.