Deploying MERN on a VPS: What They Don't Tell You
I wanted to learn 'real' deployment instead of using Vercel. Three weeks later, I had opinions.
Vercel is stupid easy. Push your code, get a URL, done. Same with Railway and Render. They handle everything for you.
But I wanted to know what was actually happening under the hood. So I rented a VPS and decided to deploy my MERN app the old-fashioned way.
Took me three weeks to get everything working properly. I learned a lot. I also developed some strong opinions.
Picking a Server
I went with Hetzner because everyone on Reddit said they were cheap and reliable. Got their basic plan: $5/month for 2GB RAM and 1 CPU core. Seemed like plenty for a small side project.
Spoiler: it wasn't.
MongoDB alone wants at least 1GB of RAM to run comfortably. Node takes another chunk. Then you need space for the React build process, which can spike during compilation. I was constantly running out of memory and the build would just die halfway through.
If you're actually doing this, start with at least $10-12/month for a server with 4GB RAM. Save yourself the headache.
The First Login Was Intimidating
Fresh server, just a command prompt. No GUI, no helpful tooltips, just root@server:~# staring at me.
I had no clue where to even start.
After reading way too many blog posts and DigitalOcean tutorials, here's the order that actually made sense:
- Update all the packages (
apt update && apt upgrade) - Create a new user (running everything as root is apparently a bad idea)
- Set up SSH keys and disable password login
- Configure the firewall to only allow ports I actually need
- Install fail2ban to block brute force attacks
The security stuff felt excessive at first. Who's going to attack my tiny personal project server?
Then I checked the auth logs a few hours later. Literally thousands of login attempts from random IP addresses in China, Russia, Eastern Europe. Bots just scanning the internet for open SSH ports and trying common passwords.
Kind of eye-opening. The internet is a hostile place.
Installing All the Things
Next step: get Node, MongoDB, Nginx, and PM2 installed. Each one has its own multi-step process.
MongoDB especially was annoying. You can't just apt install mongodb. You have to add their repository, import their GPG key, update your sources list, and then install. Six different commands just to get a database running.
I made a bash script to automate the whole setup process. Took me longer to write the script than to just do it manually once, but now if I ever need to recreate the server or set up a new one, I can just run the script.
Future me will thank present me. Probably.
Making Nginx Work
Your Node app runs on port 3000 or 5000 or whatever. But normal people expect websites to be on port 80 for HTTP and 443 for HTTPS. You don't want users typing :3000 at the end of your URL.
That's where Nginx comes in. It listens on port 80/443 and forwards requests to your Node app. Called a reverse proxy.
Basic config looked like this:
server {
listen 80;
server_name myapp.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;
}
}
Straightforward enough. Except my app uses WebSockets for real-time updates, and those weren't working. Spent an entire evening debugging before I found out you need those Upgrade and Connection headers for WebSocket support.
Added those two lines, everything started working. Cool.
SSL Certificates (Easier Than Expected)
HTTPS used to be this complicated thing with paid certificates and manual renewal. Now Let's Encrypt gives you free SSL and Certbot automates the whole process.
Just run certbot --nginx, answer a few questions about your domain, and it modifies your Nginx config automatically. Your site is now HTTPS.
One catch: the certificates expire every 90 days. Certbot sets up auto-renewal, but I've heard stories of it failing silently and people suddenly getting browser warnings about expired certificates. I added a calendar reminder to manually check every few months just in case.
Keeping the App Running
If your Node process crashes, your app goes down. Obviously not great.
PM2 solves this. It's a process manager that automatically restarts your app if it crashes, handles logging, and can even do zero-downtime deployments if you set it up right.
pm2 start server.js --name myapp
pm2 save
pm2 startup
That last command creates a startup script so PM2 and your app come back up if the server reboots.
Now I can actually close my SSH session without the app dying. Revolutionary.
The Deployment Problem
At first, I deployed by SSHing into the server, running git pull, then npm install, then restarting PM2. Every. Single. Time.
It worked, but it was tedious. And I kept forgetting steps. Or I'd run the commands in the wrong order and something would break.
Eventually I wrote a deploy script that GitHub Actions runs whenever I push to main. The workflow SSHs into the server and runs all the commands automatically. Tests pass, code deploys. No manual work.
Still not as smooth as Vercel where you just get automatic preview deployments and rollbacks and all that. But at least I understand what's actually happening now.
How I Know When Things Break
For a while, the server could have been down for hours and I wouldn't have known unless I happened to check.
Set up UptimeRobot on their free tier. It pings my site every 5 minutes and sends me an email if it's unreachable. Not sophisticated, but it works.
For actual server metrics like CPU and memory usage, I just use htop when I SSH in. I know there are proper monitoring tools like Grafana and Prometheus, but that felt like overkill for a side project. Maybe someday.
Things I'd Change If I Started Over
Looking back, I would've done a few things differently:
Start with more RAM. The $5 tier is too small for MERN. Just spend the extra $5-7/month and save yourself the constant out-of-memory errors.
Use Docker from day one. I'm running everything directly on the server, which means if I ever want to move to a different VPS, I have to reconfigure everything. With Docker, I could just move the containers.
Set up automated backups immediately. I didn't think about this until I was already live. Then I realized if the server crashed or I messed something up, I had no way to recover. Now I have daily backups, but I should've done that before launch.
Use a managed database. Running MongoDB myself means I'm responsible for updates, backups, security, everything. I've had to restore from backup twice now. Both times were stressful. A managed service like MongoDB Atlas would handle that for me. Worth the extra cost.
Was This Whole Thing Worth It?
Honestly? I'm not sure.
On one hand, I learned a ton. I actually understand what happens when you deploy an app now. I can troubleshoot server issues instead of just clicking buttons and hoping. That knowledge feels valuable.
On the other hand, it took three weeks to set up something that would've taken 10 minutes on Vercel. And I still spend time maintaining the server, updating packages, checking logs, making sure everything's running smoothly.
For most projects, especially client work or anything on a deadline, managed platforms are absolutely the right choice. Less maintenance, better uptime, more time to actually build features.
But I think every developer should go through the VPS deployment process at least once. It demystifies a lot of the "magic" that hosting platforms do behind the scenes. Plus the knowledge transfers to other areas. Understanding servers helps you write better code.
Would I do it again for my next project? Probably not. But I'm glad I did it once.