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 very easy. Push your code, get a link, and you're done. Railway and Render are the same. They take care of everything for you.
But I wanted to know what was really going on behind the scenes. I rented a VPS and decided to set up my MERN application the old-fashioned way.
Took me three weeks to get everything up and running. I picked up a lot along the way. I also formed some strong views, on things.
Picking a Server
I chose Hetzner because people on Reddit said they were affordable and worked well. I got the plan from Hetzner for five dollars per month. This plan gives you two gigabytes of RAM and one CPU core. I thought this would be enough for my project.
It was not enough.
MongoDB needs least one gigabyte of RAM to work properly. Node takes some of the RAM too. Then you need some space for the React build process. This process can use a lot of memory when it is compiling. I always ran out of memory. The build would stop working halfway through.
If you are really doing this you should start with a server that costs least ten to twelve dollars, per month. This server should have least four gigabytes of RAM. This will save you a lot of trouble with Hetzner.
The First Login Was Intimidating
I got a server and all I saw was a command prompt. There was no graphical user interface and no helpful tips to guide me. I was faced with root@server:~# and I had to figure out what to do.
I did not know where to start.
I read a lot of blog posts and DigitalOcean tutorials. After that I found out what I had to do. Here is what I did:
- I updated all the packages by using
update && apt upgrade. - I created an user because running everything as root is not a good idea.
- I set up SSH keys and I disabled password login.
- I configured the firewall to only allow the ports that my server actually needs.
- I installed fail2ban to block attacks from people who try to guess my password times.
At first I thought all the security measures were too much for my personal project server. I thought who would want to attack my server.
Then I checked the login logs a hours later. What I saw was shocking. There were thousands of login attempts from different internet addresses in China, Russia and Eastern Europe. These were automated programs that scan the internet for open SSH ports and try common passwords.
This was a wake up call for me. The internet can be a hostile place, for my server.
Installing All the Things
The next thing to do is to get Node.js and MongoDB Nginx and PM2 installed. Each one needs to be set up in a steps.
MongoDB was really frustrating to install. You can't just install it with a command. First you need to add their repository then import their GPG key update your list of sources and after that you can install it. That is six commands just to get a database up and running.
I created a bash script to make the whole setup process easier. It took me longer to write the script than to do it manually. Now if I need to set up a new server or recreate this one I can just run the script.
Future me will probably be thankful to present me for doing this.
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. I spent an evening trying to figure out what was wrong. Then I found out that you need to add the Upgrade and Connection headers, for WebSocket support.
I added those two lines and everything started working. That was pretty cool.
SSL Certificates Are Actually Simple
I used to think HTTPS was a big deal with expensive certificates and a lot of work to renew them.. Now SSL certificates are free thanks to Lets Encrypt and Certbot does all the work for you.
You just have to run certbot --nginx and answer a questions about your domain. Then Certbot changes your Nginx settings for you. After that your website is secure with HTTPS.
There is one thing to watch out for: SSL certificates last, for 90 days. Certbot is supposed to renew them but I have heard that sometimes it does not work right and people get warnings in their browser about expired SSL certificates. So I made a note on my calendar to check on it every months just to be safe. SSL certificates are easy to get with Lets Encrypt and Certbot. You have to keep an eye on them.
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. This is really great.
The Deployment Problem
When I first started I would deploy my project by logging into the server using SSH then I would run git pull followed by npm install. Finally I would restart PM2. I had to do this every time.
It was a working method. It was really boring and time consuming. I would often forget some of the steps. I would do them in the wrong order and that would cause problems.
So I decided to create a script that would deploy my project automatically. I set it up with GitHub Actions so now it runs every time I push my changes to the branch as part of my DevOps deployment workflow. This workflow logs into the server. Runs all the necessary commands for me. I wrote about how I did this in my guide on setting up CI/CD for MERN with GitHub Actions, with GitHub Actions. Now my tests. My code gets deployed without me having to do anything manually.
My deployment process is still not as easy to use as Vercel. With Vercel you get preview deployments and rollbacks which's really nice. At now I know what is going on with my deployment process and that is a big improvement.
How I Know When Things Break
For a while my server could have been down for hours and I would not have known about it unless I happened to check the server.
I set up UptimeRobot on their tier. UptimeRobot pings my site every 5 minutes. Sends me an email if my site is unreachable. This is not a solution but it works for me.
For server metrics like CPU usage and memory usage I just use htop when I log in to my server using SSH. I know that there are monitoring tools, like Grafana and Prometheus but these tools felt like overkill for my side project. Maybe I will use them someday for my side project.
Things I'd Change If I Started Over
If I had to start over again there are a few things I would do differently:
Start with RAM. The five dollar tier is too small for MERN. Just spend the five to seven dollars per month and save yourself the constant out of memory errors. This will really help you in the run.
You will have problems with your MERN application.
Use Docker from day one. I am running everything directly on the server. This means if I ever want to move to a virtual private server I have to reconfigure everything. This is a lot of work. With Docker I could just move the Docker containers. This would make my life much easier. I would not have to reconfigure everything.
Set up automated backups immediately. I did not think about this until I was already live with my application. Then I realized if the server crashed or I messed something up I had no way to recover my application. Now I have backups of my MERN application but I should have done that before I launched it.
Use a managed database. Running MongoDB myself means I am responsible for updates and backups and security and everything. I have had to restore from backup now for my MongoDB database. Both times were very stressful for me. A managed service like MongoDB Atlas would handle that for me and my MongoDB database. It is worth the cost, for my MongoDB database.
Was This Whole Thing Worth It?
To be honest I am still unsure.
On one hand I gained a lot of knowledge. I now actually know what happens when you deploy an application. I can troubleshoot server problems of just clicking buttons and hoping for the best. That knowledge seems valuable to me.
On the hand it took me three weeks to set up something that would have taken only 10 minutes on Vercel.. I still spend time maintaining the server, updating packages checking logs and making sure everything is running smoothly.
For projects, especially client work or anything with a deadline managed platforms are definitely the right choice. They require maintenance offer better uptime and give you more time to actually build features.
However I think every developer should go through the VPS deployment process least once. It helps you understand a lot of the "magic" that hosting platforms do behind the scenes.. The knowledge you gain transfers, to other areas. Understanding servers helps you write code and deploy applications.
Would I do it again for my project? Probably not.. I am glad I did it once and I learned server deployment.