Setting Up CI/CD for MERN: My GitHub Actions Journey
Automated deployments sounded scary until I actually set them up. Here's how I went from manual FTP uploads to proper CI/CD.
For a while there, I was just uploading files via FTP to deploy my MERN applications. You know, the ones in my projects folder. And this was even in 2024. It feels kind of silly now, I guess.
Deploying like that always got me anxious. Like, did I upload the right API route this time? Or put stuff in the wrong spot by mistake? Sometimes I worried I broke everything in production just because of some forgotten environment variable. It seems dumb, but it happened a lot.
The whole thing was super stressful and full of mistakes. I mean, error-prone is the word for it. And it got embarrassing too, especially when other devs would ask how I handled deployments. They probably thought I was behind or something.
I finally got CI/CD set up, though. Wish I had bothered with it way sooner. Years, really. That would have saved so much hassle.
Why I Avoided It For So Long
YAML configuration files scared me. They had a lot of indentation and nested structures. The syntax was also very specific. If I added one space everything would break. I thought DevOps engineers with a lot of experience could understand YAML configuration files.
I was thinking about it too much. A CI/CD workflow is really a list of things that happen automatically. When I push code the CI/CD workflow runs tests. When I merge code to the branch the CI/CD workflow deploys it to production. That is basically what a CI/CD workflow does.
Starting Small: Just Run the Tests
I did not jump straight into automated deployments. My first workflow was really simple: I just made it run tests whenever someone opens a GitHub pull request.
Here is the entire thing:
name: Test
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
I have seven lines of code that actually work. That is all.. Now if someone, including me tries to merge code that does not work the tests catch it automatically. No more saying "oh no I forgot to run tests before merging."
The Deployment Part Was Harder
Getting the tests to run was easy. Actually putting my project on my VPS was a lot harder.
I do not use Vercel or Netlify. I have a VPS where I run my Docker containers. So I need to set up my workflow to connect to my server get the code rebuild the containers and start everything again.
The part that made me feel unsure was giving GitHub access, to my production machine. What if someone gets into my repository? What if the keys get out somehow?
It turns out you can make special deploy keys that can only do a things. They can only get code. Restart certain services, nothing else.. Github keeps them secret. I still felt a bit weird the time I did it but it is actually pretty safe. My Docker containers are secure now.
I Leaked a Secret. Github Saved Me
GitHub has a feature to store values like API keys and SSH keys as secrets in your repository settings. The workflow uses the names and GitHub does not show the actual values in logs.
That is a feature. It is very useful. I still messed it up. I was trying to find out why my environment variables were not loading. So I added a step to print out my .env file.
I pushed the code. Watched the workflow run. Then I saw my database password in the logs. It was there for anyone to see. I panicked for 30 seconds. Then I noticed GitHub had automatically hidden it.
They saw it was a secret. Replaced it with *** in the output. The crisis was over.
I learned to be much more careful about what I print during debugging. I will be more careful with my secrets from, on. GitHub saved me from a mistake.
What My Deployment Actually Does
So after lots of trying and failing here's what happens when I push to the branch:
- All tests are run. If they fail everything stops.
- The React frontend is built with environment variables.
- A Docker image, with both frontend and backend is built.
- The image is pushed to Docker Hub.
- I connect to my VPS using SSH.
- I pull the image.
- The old containers are stopped.
- New containers are started with the updated image.
The whole process takes 4 minutes. Before I spent 20-30 minutes doing this manually checking every step twice and still making mistakes. Now I just merge the request and go get a cup of coffee.
Caching Made It Way Faster
At first every workflow run took a long time because npm was installing all the dependencies from scratch. This was happening every time and it was really slow. There were hundreds of packages to install.
Then I found out about caching. I learned that GitHub Actions can save your node modules and use them again if your package lock json file has not changed. This is a help because now GitHub Actions can just use the node modules it saved before instead of installing everything all over again.
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
This change really helped. It cut my build time in half. Now my system only does an installation when I add new packages or update existing ones.
Things That Went Wrong (. How I Fixed Them)
Things did not go as planned at first.. The second time.. Even the fifth time.
Using versions of Node: My tests worked on my computer. They failed when I used the continuous integration tool. I found out that I was using Node version 18 on my computer and the continuous integration tool was using Node version 16. So I added the version of Node to the integration tool and the problem was fixed.
Since my project uses Next.js and Node.js it is very important that the versions of these tools are the same on my computer and on the integration tool.
Not having MongoDB: My tests needed a local MongoDB database to run.. The continuous integration tool on GitHub does not have MongoDB. So I used a tool called mongodb-memory-server to run my tests. This tool lets my tests run without needing a MongoDB database.
Bugs that only happened on systems: Sometimes my code would work on my Mac. It would not work on the Ubuntu system used by the continuous integration tool. This was because of differences in how the systems handle file paths and case sensitivity. Now I try to write code that works the same on all systems.
Tests that did not always work: Some of my tests would work most of the time. Sometimes they would fail. This was very frustrating. I found out that these tests had problems, with timing or they relied on applications. So I fixed them to make them more reliable.
Each time something went wrong it was annoying.. I learned something new each time. Now my continuous integration tool works smoothly. I hardly ever need to make any changes to it. It just works.
The Real Benefit Is Not Speed
I think automated deployments are faster than doing things.. That is not why I like Continuous Integration and Continuous Deployment.
The real benefit is feeling confident.
Before every time I deployed something it was like taking a chance. I would send code. Hope that nothing went wrong. I would look at server logs to see if there were any errors and I was ready to go to a previous version if something did go wrong.
Now if the tests are okay and the workflow works I know the code is good. I can put the code together close my laptop. Not worry about it. Feeling calm like that is worth more than the time I save.
I Still Watch It Sometimes
Old habits are hard to break. Even though I trust the workflow I sometimes look at GitHub Actions. Watch the steps happen. I see the tests pass I watch the build finish I make sure the deployment was successful.
I do not have to do this. The workflow will tell me if something goes wrong.. It feels good to see all those checks that say everything is okay.
Maybe I will stop doing this one day.. Maybe I will not. Either way I am not going back, to uploading files with FTP.
If you are still deploying things manually start with a simple test workflow. You do not have to automate everything at the time. Get used to using YAML add one step at a time and before you know it you will wonder how you ever deployed things in any way.