Building a Jenkins CI/CD Pipeline on AWS EC2 for a 3-Tier Dockerized App
A hands-on walkthrough of automating deployments with Jenkins on two AWS EC2 instances, using GitHub webhooks, SSH, and Docker Compose, plus the real mistakes I hit and how I fixed them.
As a DevOps intern, I kept hearing that "CI/CD saves time," but I only understood it when I built a pipeline from scratch. In this article, I walk through how I set up Jenkins on AWS EC2 to automatically deploy a React + Node.js + MySQL application using Docker Compose, and the problems I faced along the way.
Architecture
- Jenkins Server (EC2 #1): runs Jenkins and pulls code from GitHub
- App Server (EC2 #2): runs the application containers
- Flow:
git push→ GitHub webhook → Jenkins pipeline → SSH to App Server →docker compose up -d --build

- I kept Jenkins and the app on separate instances so heavy builds never starve the running application.
Prerequisites
- AWS account with two EC2 instances ([instance type], [region])
- Security groups: SSH (22) and Jenkins (8080) from your IP, app ports open on the App Server
- A GitHub repo containing a
docker-compose.yml - Basic Docker knowledge
Step 1: Install Jenkins on EC2 #1
bash
1
2
3
4
sudo apt update
sudo apt install -y fontconfig openjdk-17-jre
# add the Jenkins repo and install as per the official docs
sudo systemctl enable --now jenkinsOpen
http://<jenkins-public-ip>:8080, unlock with the initial admin password, and install the suggested plugins.Step 2: Prepare the App Server (EC2 #2)
Install Docker and Docker Compose, then allow your deploy user to run Docker without sudo:
bash
1
sudo usermod -aG docker $USERStep 3: Set up SSH access from Jenkins to the App Server
Generate a key pair, add the public key to
~/.ssh/authorized_keys on the App Server, and store the private key in Jenkins credentials with an ID like app-server-ssh.Step 4: Connect GitHub with a webhook
In the repo, go to Settings → Webhooks and add
http://<jenkins-ip>:8080/github-webhook/. In the Jenkins job, enable "GitHub hook trigger for GITScm polling."Step 5: The Jenkinsfile
groovy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pipeline {
agent any
stages {
stage('Checkout') {
steps { git branch: 'main', url: 'https://github.com/<user>/<repo>.git' }
}
stage('Deploy to App Server') {
steps {
sshagent(['app-server-ssh']) {
sh '''
ssh -o StrictHostKeyChecking=no ubuntu@<app-server-ip> "
cd ~/app || git clone https://github.com/<user>/<repo>.git ~/app && cd ~/app
git pull origin main
docker compose down
docker compose up -d --build
"
'''
}
}
}
}
}Step 6: Test it
Push a small change. Jenkins triggers automatically, and the updated app goes live within a couple of minutes.

Challenges I faced
- Java version mismatch: Jenkins failed until the Java version matched its requirement.
- Disk space: Docker images quickly fill a small root volume. Use
docker system pruneand size the volume properly. - Docker permissions: "permission denied" on the Docker socket was fixed by adding the user to the
dockergroup and logging in again. - Jenkinsfile syntax errors: one missing brace cost me an hour. Use Replay and a linter.
- Master-agent vs SSH deploy: I first tried a master-agent setup, but for simple deployments SSH from the pipeline was cleaner and easier to debug.
What I'd improve next
- Move secrets into a proper secret store
- Add automated test stages before deploy
- Push images to Amazon ECR instead of building on the server
- Add a load balancer with HTTPS
Conclusion
Building this end-to-end taught me more than any tutorial, mostly because things broke along the way. If you're learning DevOps, pick a small app, break it, and fix it. Code: https://github.com/KashfaMuskan329/3tier-jenkins-cicd-pipeline.
Questions are welcome in the comments!
Questions are welcome in the comments!
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article