Hey there! If you're working on web applications and want to automate deployment, you're in the right place. Recently, I set up a Jenkins CI/CD pipeline to deploy a MERN stack application on an AWS EC2 instance, and it made the entire process effortless! Instead of manually pushing updates to the server, Jenkins now takes care of everything—from pulling the latest code to deploying it seamlessly.
In this blog, I’ll walk you through how Jenkins works, why it’s a great CI/CD tool, and how you can use it to automate your MERN stack deployment. Whether you're new to automation or looking to refine your deployment workflow, this guide will help you set up a smooth and efficient process.
Let’s get started! 🚀
Why Automate Deployment with Jenkins?
The Problem with Manual Deployment
Deploying updates manually can be frustrating and inefficient. Each time a new feature is added or a bug is fixed, developers often need to SSH into the EC2 instance, pull the latest code from GitHub, restart the application, and verify that everything is working correctly. This process is not only time-consuming but also increases the risk of human error, such as forgetting a step or introducing inconsistencies between deployments. If multiple developers are working on the project, manual deployments can lead to version conflicts and downtime, negatively affecting users.
The Solution: Automating Deployment with Jenkins
Jenkins solves this problem by automating the entire deployment process. Instead of manually updating the application, Jenkins continuously monitors the GitHub repository for changes and automatically triggers a deployment whenever new code is pushed. This ensures that:
The latest code is always deployed without manual intervention.
Deployment steps are executed consistently, reducing errors.
Developers can focus on writing code instead of worrying about deployment.
The application remains up-to-date with minimal downtime.
By integrating Jenkins into the deployment workflow, teams can achieve a faster, more reliable, and error-free deployment process.
What is Jenkins and Why is it Useful?
Jenkins is an open-source automation server that helps developers automate various stages of the software development process. It's commonly used for Continuous Integration (CI) and Continuous Delivery (CD), which are core principles in modern DevOps practices. Jenkins allows you to automate the process of building, testing, and deploying applications, reducing the need for manual intervention and ensuring a smooth and consistent workflow.
The reason Jenkins is so useful is that it eliminates many of the manual tasks that can slow down development and deployment cycles. Let’s break down why Jenkins is such a great tool for automating your deployment process:
Automation of Repetitive Tasks: Jenkins automates the entire process of building, testing, and deploying applications, saving you time and reducing the need for manual intervention.
Consistency: It ensures that the same steps are followed every time, minimizing human errors that can occur during manual deployments.
Integration with Popular Tools: Jenkins easily integrates with tools like GitHub, Docker, and AWS, making it simple to set up CI/CD pipelines for various environments.
Scalability: Jenkins can handle both small-scale and large-scale deployments, adapting to your needs as your project grows.
Time Savings: Automating deployments with Jenkins eliminates manual steps, allowing developers to focus on writing code rather than managing deployments.
In summary, Jenkins simplifies and speeds up the deployment process, ensuring reliable, error-free updates.
If you're curious to see Jenkins in action, check out my MERN stack deployment project on GitHub! 🚀 You'll find the full CI/CD pipeline, Docker configuration, and deployment process setup using Jenkins.
🔗 GitHub Repository: https://github.com/amaan-sayyed/jenkins-pipeline
To deploy the MERN stack application to your EC2 instance using Jenkins, follow these steps:
Step 1: Create a New Jenkins Pipeline Job
- Log in to your Jenkins UI.
As you can see our Jenkins is up and running and accessible at localhost:8080
On the Jenkins dashboard, click on New Item.
Enter the item name as mern-test.
Select Pipeline as the project type.
Click OK to create the new pipeline job.
On the job configuration page, scroll down to the General section.
Check the option for GitHub project.
In the Project URL field, enter the URL of your GitHub repository:
https://github.com/amaan-sayyed/jenkins-pipeline
.
Scroll down to the Build Triggers section.
Check the box for GitHub hook trigger for GITScm polling.
This will allow Jenkins to automatically trigger builds whenever there is a push to your GitHub repository, using the webhook from GitHub.
Scroll down to the Pipeline section in the job configuration page.
Set the Definition field to Pipeline script.
In the Pipeline script box, paste the following script:
pipeline {
agent any
stages {
stage("Clone Repository") {
steps {
echo "Cloning the repository"
git url: 'https://github.com/amaan-sayyed/jenkins-pipeline.git', branch: 'main'
}
}
stage("Build and Push Backend Image") {
steps {
echo "Building and pushing the backend image"
withCredentials([usernamePassword(credentialsId: "dockerHub", passwordVariable: "DOCKER_HUB_PASS", usernameVariable: "DOCKER_HUB_USER")]) {
dir('./mern/backend'){
sh 'docker build -t ${DOCKER_HUB_USER}/mern-backend:latest .'
sh 'docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}'
sh 'docker push ${DOCKER_HUB_USER}/mern-backend:latest'
}
}
}
}
stage("Build and Push Frontend Image") {
steps {
echo "Building and pushing the frontend image"
withCredentials([usernamePassword(credentialsId: "dockerHub", passwordVariable: "DOCKER_HUB_PASS", usernameVariable: "DOCKER_HUB_USER")]) {
dir('./mern/frontend'){
sh 'docker build -t ${DOCKER_HUB_USER}/mern-frontend:latest .'
sh 'docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}'
sh 'docker push ${DOCKER_HUB_USER}/mern-frontend:latest'
}
}
}
}
stage("Deploy to EC2") {
steps {
echo "Deploying to EC2"
withCredentials([sshUserPrivateKey(credentialsId: 'ec2-ssh-key', keyFileVariable: 'SSH_KEY')]) {
sh """
mkdir -p ~/.ssh
ssh-keyscan -H YOUR_IP_ADDRESS >> ~/.ssh/known_hosts
scp -i $SSH_KEY docker-compose.yaml ubuntu@YOUR_IP_ADDRESS:/home/ubuntu/
scp -i $SSH_KEY startup-script.sh ubuntu@YOUR_IP_ADDRESS:/home/ubuntu/
ssh -i $SSH_KEY ubuntu@YOUR_IP_ADDRESS 'bash /home/ubuntu/startup-script.sh'
"""
}
}
}
}
}
Step 2: Building Docker Images and pushing them to Dockerhub.
In Step 2, we break down the process into three main stages: cloning the GitHub repository, building the backend Docker image, and building the frontend Docker image. Here's an explanation of each stage:
Stage 1: Clone the Repository
stage("Clone Repository") {
steps {
echo "Cloning the repository"
git url: 'https://github.com/amaan-sayyed/jenkins-pipeline.git', branch: 'main'
}
}
Purpose: This stage ensures that Jenkins fetches the latest code from your GitHub repository.
What Happens:
The
git
command is used to clone the repository from GitHub. It pulls the code from the main branch of the specified repository (https://github.com/amaan-sayyed/jenkins-pipeline.git
).The
echo
command is used for logging purposes to display a message in the Jenkins console that the repository is being cloned.
Console Output:
Here’s how the Jenkins console output will look once the repository is cloned:
In the image above, you can see that Jenkins successfully clones the repository, and the log shows the process of checking out the main
branch.
Before building and pushing the Docker images, you need to set up the necessary credentials for Docker Hub in Jenkins. Here's how you can do that:
Steps:
Navigate to Jenkins UI: Go to Manage Jenkins > Manage Credentials.
Add New Credentials:
Select (global) under Stores scoped to Jenkins.
Click on Add Credentials.
In the Kind dropdown, choose Username with password.
Enter your Docker Hub username and password in the respective fields.
Set the ID to
dockerHub
(or any ID you prefer, but ensure it matches the one used in the Jenkins pipeline script).
Save the credentials.
Stage 2: Build and Push the Backend Docker Image
stage("Build and Push Backend Image") {
steps {
echo "Building and pushing the backend image"
withCredentials([usernamePassword(credentialsId: "dockerHub", passwordVariable: "DOCKER_HUB_PASS", usernameVariable: "DOCKER_HUB_USER")]) {
dir('./mern/backend') {
sh 'docker build -t ${DOCKER_HUB_USER}/mern-backend:latest .'
sh 'docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}'
sh 'docker push ${DOCKER_HUB_USER}/mern-backend:latest'
}
}
}
}
Purpose: This stage builds the backend Docker image from the
./mern/backend
directory and pushes it to Docker Hub.What Happens:
The
withCredentials
block securely provides Docker Hub credentials stored in Jenkins (dockerHub
), allowing Jenkins to authenticate with Docker Hub.The
dir
block navigates to the./mern/backend
directory, where the backend Dockerfile is located.The
sh
command executes the following:docker build -t ${DOCKER_HUB_USER}/mern-backend:latest .
: Builds the Docker image for the backend application and tags it aslatest
.docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}
: Authenticates Jenkins with Docker Hub using the credentials.docker push ${DOCKER_HUB_USER}/mern-backend:latest
: Pushes the backend image to the Docker Hub repository.
Console Output:
Here’s how the Jenkins console output will look once the backend image is being built and pushed to Docker Hub:
In the image above, you can see that Jenkins builds the backend Docker image and pushes it to Docker Hub, showing each step in the console log.
Docker Hub UI:
Once the backend image is pushed, you should be able to see it in the Docker Hub UI under your repository. Here’s a screenshot of the backend image displayed in Docker Hub:
Stage 3: Build and Push the Frontend Docker Image
stage("Build and Push Frontend Image") {
steps {
echo "Building and pushing the frontend image"
withCredentials([usernamePassword(credentialsId: "dockerHub", passwordVariable: "DOCKER_HUB_PASS", usernameVariable: "DOCKER_HUB_USER")]) {
dir('./mern/frontend') {
sh 'docker build -t ${DOCKER_HUB_USER}/mern-frontend:latest .'
sh 'docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}'
sh 'docker push ${DOCKER_HUB_USER}/mern-frontend:latest'
}
}
}
}
Purpose: This stage builds the frontend Docker image from the
./mern/frontend
directory and pushes it to Docker Hub.What Happens:
Similar to the backend image, the
withCredentials
block is used to access Docker Hub credentials.The
dir
block changes the directory to./mern/frontend
where the frontend Dockerfile is located.The
sh
command does the following:docker build -t ${DOCKER_HUB_USER}/mern-frontend:latest .
: Builds the frontend Docker image and tags it aslatest
.docker login -u ${DOCKER_HUB_USER} -p ${DOCKER_HUB_PASS}
: Logs in to Docker Hub with the provided credentials.docker push ${DOCKER_HUB_USER}/mern-frontend:latest
: Pushes the frontend image to Docker Hub.
Console Output:
Here’s how the Jenkins console output will look once the frontend image is being built and pushed to Docker Hub:
In the image above, you can see that Jenkins builds the backend Docker image and pushes it to Docker Hub, showing each step in the console log.
Docker Hub UI:
Once the frontend image is pushed, you can also see it in Docker Hub. Here’s a screenshot of the frontend image displayed in Docker Hub:
Step 3: Deploy to EC2
stage("Deploy to EC2") {
steps {
echo "Deploying to EC2"
withCredentials([sshUserPrivateKey(credentialsId: 'ec2-ssh-key', keyFileVariable: 'SSH_KEY')]) {
sh """
mkdir -p ~/.ssh
ssh-keyscan -H YOUR_IP_ADDRESS >> ~/.ssh/known_hosts
scp -i $SSH_KEY docker-compose.yaml ubuntu@YOUR_IP_ADDRESS:/home/ubuntu/
scp -i $SSH_KEY startup-script.sh ubuntu@YOUR_IP_ADDRESS:/home/ubuntu/
ssh -i $SSH_KEY ubuntu@YOUR_IP_ADDRESS 'bash /home/ubuntu/startup-script.sh'
"""
}
}
}
Make sure to replace YOUR_EC2_IP_ADDRESS
with the actual IP address of your EC2 instance.
After running the pipeline, Jenkins will display a Stage View that shows the status of each stage in the process. When all stages pass successfully, you’ll see a visual confirmation of a completed pipeline with green checkmarks for each stage.
Stage View:
Here’s a screenshot of the Stage View in Jenkins, indicating that all the stages have successfully completed:
In the image above, you can see that all stages (Cloning the repository, Building and pushing backend and frontend images, Deploying to EC2) have completed successfully, and the pipeline has been executed without errors.
Explanation:
This stage is responsible for deploying the MERN stack application to an AWS EC2 instance. Here's what each line in the script does:
withCredentials([sshUserPrivateKey(credentialsId: 'ec2-ssh-key', keyFileVariable: 'SSH_KEY')])
:- This step ensures that Jenkins uses SSH credentials stored in Jenkins' Credentials Manager to authenticate to the EC2 instance. The credentials
ec2-ssh-key
should be set up beforehand in Jenkins to securely store the private SSH key used for accessing the EC2 instance.
- This step ensures that Jenkins uses SSH credentials stored in Jenkins' Credentials Manager to authenticate to the EC2 instance. The credentials
mkdir -p ~/.ssh
:- This command creates the
~/.ssh
directory on the Jenkins server if it doesn't already exist. This directory is where SSH configuration files, including known hosts, are stored.
- This command creates the
ssh-keyscan -H 54.157.194.36 >> ~/.ssh/known_hosts
:- This command adds the EC2 instance’s IP address (
54.157.194.36
) to the known hosts file, which prevents SSH from asking if you trust the server every time it connects. This is important for automated deployments.
- This command adds the EC2 instance’s IP address (
scp -i $SSH_KEY docker-compose.yaml ubuntu@54.157.194.36:/home/ubuntu/
:- This command securely copies the
docker-compose.yaml
file from the Jenkins server to the EC2 instance's/home/ubuntu/
directory using the specified private SSH key ($SSH_KEY
).
- This command securely copies the
scp -i $SSH_KEY
startup-script.sh
ubuntu@54.157.194.36:/home/ubuntu/
:- Similarly, this command copies the
startup-script.sh
file to the EC2 instance. This script is used to automate the startup and execution of Docker containers once they are deployed to the instance.
- Similarly, this command copies the
ssh -i $SSH_KEY ubuntu@54.157.194.36 'bash /home/ubuntu/
startup-script.sh
'
:This command SSHs into the EC2 instance and runs the
startup-script.sh
script. This script could include commands to:Pull the latest Docker images.
Set up and start the Docker containers using Docker Compose.
Restart any necessary services.
Console Output:
Here’s a screenshot of the Jenkins console log showing the deployment process:
In the image above, you can see the logs that confirm the deployment steps, including SSH connection to the EC2 instance, file transfer, and execution of the startup script.
Pre-Requisites for EC2 Deployment:
Add SSH Rules to the EC2 Instance's Security Group:
- Before the Jenkins pipeline can connect to the EC2 instance via SSH, you need to ensure that the EC2 security group allows incoming SSH connections. This can be done by adding an inbound rule for SSH (port 22) from the Jenkins server’s IP address.
Add Inbound Rule for Port 5173:
- If you need to access the frontend application running on your EC2 instance, make sure to open port 5173 (or the appropriate port for your app) in the EC2 security group to allow external access to the app.
Install Docker and Docker-Compose:
- Ensure that Docker and Docker Compose are pre-installed on the EC2 instance before running the pipeline. These tools are required to run the MERN stack application in containers.
Startup Script:
The
startup-script.sh
that you copy to the EC2 instance is a shell script that can automate tasks like pulling Docker images and starting containers. For example, it may contain commands like:docker-compose -f docker-compose.yaml up -d
This copies the startup-script and the docker-compose file to the deployment server.
As you can see, our application is up and running in the deployment server and our pipeline has been successfully executed.
And Finally lets access our application using the server’s public IP at port 5173.
And there you go!! We have successfully deployed our 3-tier MERN Stack application to a remote EC2 Instance which acts like a Deployment server using GitHub Actions.
Conclusion:
In conclusion, setting up a CI/CD pipeline with Jenkins for a MERN stack application simplifies and automates the deployment process. By using Jenkins to automate tasks like cloning the repository, building Docker images, pushing them to Docker Hub, and deploying the application to an EC2 instance, we ensure that the app is always up-to-date and deployed consistently without manual intervention. The use of Jenkins credentials for secure access to Docker Hub and the EC2 instance adds an extra layer of safety for sensitive information. This automated workflow not only boosts development productivity but also provides a reliable, efficient, and scalable solution for continuous integration and delivery.