Deploy a Flask backend (port 5000) and Express frontend (port 3000) on a single EC2 instance with a Jenkins CI/CD pipeline.
GitHub Repository: flask-express-cicd-jenkins-github-webhooks-aws-ec2
graph TD
Browser([🌐 Your Browser])
Browser -->|HTTP :3000| Express
subgraph EC2 ["☁️ AWS EC2 Instance — Ubuntu 22.04"]
Express["🟩 Express Frontend\nport 3000 · pm2"]
Flask["🐍 Flask Backend\nport 5000 · pm2"]
Jenkins["⚙️ Jenkins CI/CD\nport 8080"]
Express -->|POST /process| Flask
end
subgraph GitHub ["🐙 GitHub"]
Repo1[flask-express-cicd-jenkins-github-webhooks-aws-ec2]
end
GitHub -->|Webhook on git push| Jenkins
Jenkins -->|git pull + pm2 restart| Express
Jenkins -->|git pull + pm2 restart| Flask
flowchart LR
Dev([👨💻 Developer])-->|git push| GH[(GitHub Repo)]
GH -->|webhook trigger| J[⚙️ Jenkins]
J --> S1["1️⃣ Checkout\ngit pull"]
S1 --> S2["2️⃣ Install Deps\npip / npm install"]
S2 --> S3["3️⃣ Test\npytest / npm test"]
S3 --> S4["4️⃣ Deploy\npm2 restart"]
S4 --> App(["✅ App Updated"])
| Port | Protocol | Purpose |
|---|---|---|
22 |
TCP | SSH access |
3000 |
TCP | Express Frontend |
5000 |
TCP | Flask Backend |
8080 |
TCP | Jenkins UI |
Run both apps locally before deploying to EC2.
Flask — Python venv:
cd flask-backend
# Create venv
python -m venv venv
# Activate (Windows)
venv\Scripts\activate
# Activate (Mac/Linux)
source venv/bin/activate
# Install dependencies
pip install -r requirements.txtYour terminal prefix will show (venv) when the virtual environment is active.
Re-run
venv\Scripts\activate(Windows) orsource venv/bin/activate(Mac/Linux) every time you open a new terminal for Flask.
Express — Node.js (no venv needed):
Node.js doesn't need a virtual environment — npm install installs dependencies locally inside node_modules/, which is already project-scoped.
cd express-frontend
npm installOpen two terminals in VS Code (Ctrl+Shift+5 to split):
Terminal 1 — Flask backend (with venv active):
cd flask-backend
# Activate venv first
venv\Scripts\activate # Windows
# source venv/bin/activate # Mac/Linux
python app.pyExpected: Running on http://0.0.0.0:5000
Terminal 2 — Express frontend:
cd express-frontend
node app.jsExpected: Express app listening on port 3000
Open http://localhost:3000 in your browser, fill in the form, and submit.
Express forwards the form data to Flask at http://localhost:5000/process and displays the response.
Or test Flask directly:
curl -X POST http://localhost:5000/process \
-H "Content-Type: application/json" \
-d "{\"name\":\"John\",\"message\":\"Hello\"}"deactivateBrowser → Express :3000 (serves form)
│
│ POST /submit (form data)
▼
Flask :5000 /process (processes & replies)
│
▼
Express returns Flask response to browser
- AMI: Ubuntu 22.04 LTS (free-tier:
t2.micro) - Open inbound ports:
22,3000,5000,8080 - Download the
.pemkey pair
chmod 400 your-key.pem
ssh -i your-key.pem ubuntu@<EC2_PUBLIC_IP>sudo apt update && sudo apt upgrade -y
# Python
sudo apt install -y python3 python3-pip python3-venv
# Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
# pm2 & Git
sudo npm install -g pm2
sudo apt install -y gitcd ~
git clone https://github.com/shubhmate/flask-express-cicd-jenkins-github-webhooks-aws-ec2.git
cd flask-express-cicd-jenkins-github-webhooks-aws-ec2cd flask-backend
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
deactivate
pm2 start venv/bin/python --name flask-backend -- app.py
pm2 saveVerify: curl http://<EC2_PUBLIC_IP>:5000/
cd ~/flask-express-cicd-jenkins-github-webhooks-aws-ec2/express-frontend
npm install
pm2 start app.js --name express-frontend
pm2 saveVerify: curl http://<EC2_PUBLIC_IP>:3000/
pm2 startup
# Run the command pm2 outputs, then:
pm2 saveWhat is CI/CD? CI/CD stands for Continuous Integration / Continuous Deployment. Instead of manually SSHing into your server and restarting apps every time you push code, Jenkins watches your GitHub repo and does it automatically. Every
git pushtriggers Jenkins to pull the latest code, install dependencies, run tests, and restart the app.
SSH into your EC2 instance and run the following commands one by one:
# Add Jenkins GPG key so apt trusts the Jenkins package
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
# Add Jenkins to apt sources list
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
https://pkg.jenkins.io/debian-stable binary/" | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
# Update apt and install Jenkins + Java (Jenkins requires Java to run)
sudo apt update
sudo apt install -y jenkins openjdk-17-jdk
# Start Jenkins and enable it to auto-start on reboot
sudo systemctl enable --now jenkinsVerify Jenkins is running:
sudo systemctl status jenkins
# You should see: Active: active (running)- Open your browser and go to
http://<EC2_PUBLIC_IP>:8080 - You will see an Unlock Jenkins screen asking for an initial admin password
- Get the password from your EC2 terminal:
sudo cat /var/lib/jenkins/secrets/initialAdminPassword- Copy the output and paste it into the browser
- Click Continue
After unlocking, Jenkins will ask you to install plugins:
- Click Install suggested plugins — wait for it to finish (takes 1-2 minutes)
- Once done, create your admin username and password when prompted
- Click Save and Finish → Start using Jenkins
Now install additional required plugins:
- Go to Manage Jenkins (left sidebar) → Plugins
- Click the Available plugins tab
- Search and install each of these (tick the checkbox, then click Install):
GitNodeJSPipeline
- Tick Restart Jenkins when installation is complete
Jenkins needs to know where Node.js is for the Express pipeline:
- Go to Manage Jenkins → Tools
- Scroll down to NodeJS installations → click Add NodeJS
- Fill in:
- Name:
NodeJS20 - Version:
20.x(select from dropdown)
- Name:
- Click Save
By default, Jenkins runs as its own system user (jenkins) and cannot run pm2 commands which are owned by the ubuntu user. Fix this:
# Add jenkins user to the ubuntu group
sudo usermod -aG ubuntu jenkinsNow give Jenkins permission to run pm2 as the ubuntu user without a password prompt:
# Open the sudoers file safely
sudo visudoScroll to the bottom of the file and add this line:
jenkins ALL=(ubuntu) NOPASSWD: /usr/bin/pm2
Note:
(ubuntu)means Jenkins can run pm2 as the ubuntu user viasudo -u ubuntu pm2. This keeps apps running under ubuntu's pm2 daemon (accessible publicly) rather than Jenkins' own isolated pm2 daemon.
Save and exit (Ctrl+X → Y → Enter if using nano).
Restart Jenkins to apply the group change:
sudo systemctl restart jenkinsThis creates a Jenkins job that watches your repo and runs flask-backend/Jenkinsfile.
- On the Jenkins dashboard, click New Item
- Enter name:
flask-backend - Select Pipeline → click OK
- On the configuration page:
- Scroll to Build Triggers → tick GitHub hook trigger for GITScm polling
- Scroll to Pipeline section
- Set Definition to
Pipeline script from SCM - Set SCM to
Git - Enter repo URL:
https://github.com/shubhmate/flask-express-cicd-jenkins-github-webhooks-aws-ec2.git - Set Branch to
*/main - Set Script Path to
flask-backend/Jenkinsfile
- Click Save
The Jenkinsfile uses
checkout scminstead of a hardcoded repo URL. This means it reads the repo URL and branch directly from the Jenkins job configuration above — so if you ever change the repo URL in Jenkins, the Jenkinsfile doesn't need to be updated.
- Click New Item
- Enter name:
express-frontend - Select Pipeline → click OK
- On the configuration page:
- Scroll to Build Triggers → tick GitHub hook trigger for GITScm polling
- Scroll to Pipeline section
- Set Definition to
Pipeline script from SCM - Set SCM to
Git - Enter repo URL:
https://github.com/shubhmate/flask-express-cicd-jenkins-github-webhooks-aws-ec2.git - Set Branch to
*/main - Set Script Path to
express-frontend/Jenkinsfile
- Click Save
Same as Flask —
checkout scmin the Jenkinsfile picks up the repo URL and branch from this job config automatically.
A webhook tells GitHub to notify Jenkins every time you push code, so the pipeline runs automatically.
In your GitHub repository:
- Go to your repo → Settings tab
- Click Webhooks in the left sidebar → Add webhook
- Fill in:
- Payload URL:
http://<EC2_PUBLIC_IP>:8080/github-webhook/ - Content type:
application/json - Which events: select
Just the push event
- Payload URL:
- Click Add webhook
- GitHub will send a test ping — you should see a green tick ✅ next to the webhook
Only one webhook needed — both pipelines are in the same repo.
Trigger a build manually first to make sure everything works:
- Go to your Jenkins dashboard
- Click on
flask-backendjob → click Build Now (left sidebar) - Click on the build number (e.g.
#1) under Build History - Click Console Output to watch the live logs
You should see each stage complete:
[Checkout] ✔
[Install Dependencies] ✔
[Test] ✔
[Deploy] ✔
Finished: SUCCESS
Repeat for express-frontend.
Now try the full automation — make a small change to your code, push to GitHub, and watch Jenkins trigger automatically within seconds.
Add your screenshots inside the
screenshots/folder using the filenames below. They will render here automatically.
AWS Console → EC2 → Instances → Instance State: Running
AWS Console → EC2 → Security Groups → Inbound rules showing ports
22,3000,5000,8080
Your terminal showing
ubuntu@ip-...after SSH login
Run
pm2 listin EC2 terminal — bothflask-backendandexpress-frontendshould show online
Open
http://<EC2_PUBLIC_IP>:5000/in browser
Open
http://<EC2_PUBLIC_IP>:3000/in browser
Fill in the form and submit — Flask response should appear on screen
Open
http://<EC2_PUBLIC_IP>:8080— bothflask-backendandexpress-frontendjobs visible
Jenkins → flask-backend job → Stage View showing all 4 stages passed ✅
Jenkins → express-frontend job → Stage View showing all 4 stages passed ✅
Jenkins → any build → Console Output → scroll to bottom showing
Finished: SUCCESS
GitHub → Repo Settings → Webhooks → green tick ✅ showing successful delivery
GitHub Push
│
▼
Jenkins Webhook Trigger
│
├── Checkout (git pull latest code)
├── Install Dependencies (pip install / npm install)
├── Test (pytest / npm test)
└── Deploy (pm2 restart)
flask-express-cicd-jenkins-github-webhooks-aws-ec2/
├── .gitignore
├── README.md
├── flask-backend/
│ ├── app.py
│ ├── requirements.txt
│ ├── .gitignore
│ └── Jenkinsfile
├── express-frontend/
│ ├── app.js
│ ├── package.json
│ ├── .gitignore
│ ├── templates/
│ │ └── index.html
│ └── Jenkinsfile
└── screenshots/
├── part1/
└── part2/
| Service | URL |
|---|---|
| Flask | http://<EC2_PUBLIC_IP>:5000/ |
| Express | http://<EC2_PUBLIC_IP>:3000/ |
| Jenkins | http://<EC2_PUBLIC_IP>:8080/ |
Check running processes: pm2 list
Error:
W: GPG error: https://pkg.jenkins.io/debian-stable binary/ Release:
The following signatures couldn't be verified because the public key
is not available: NO_PUBKEY 7198F4B714ABFC68
E: The repository 'https://pkg.jenkins.io/debian-stable binary/ Release' is not signed.
Cause: The old key URL jenkins.io-2023.key is expired/invalid. Jenkins updated their GPG key.
Fix: Use the updated jenkins.io-2026.key URL:
# Step 1: Remove old broken key and repo entry
sudo rm -f /usr/share/keyrings/jenkins-keyring.asc
sudo rm -f /etc/apt/sources.list.d/jenkins.list
# Step 2: Download the updated key
sudo wget -O /usr/share/keyrings/jenkins-keyring.asc \
https://pkg.jenkins.io/debian-stable/jenkins.io-2026.key
# Step 3: Add the Jenkins repository
echo "deb [signed-by=/usr/share/keyrings/jenkins-keyring.asc] \
https://pkg.jenkins.io/debian-stable binary/" | sudo tee \
/etc/apt/sources.list.d/jenkins.list > /dev/null
# Step 4: Update and install
sudo apt update
sudo apt install -y jenkins openjdk-17-jdk
# Step 5: Start Jenkins
sudo systemctl enable --now jenkinsError:
Active: failed (Result: exit-code)
Process: ExecStart=/usr/bin/jenkins (code=exited, status=1/FAILURE)
Cause: Java not installed or wrong Java version. Jenkins requires Java 17.
Fix:
# Check Java version
java -version
# Install Java 17
sudo apt install -y openjdk-17-jdk
# If multiple Java versions exist, set Java 17 as default
sudo update-alternatives --config java
# Type the number for Java 17 and press Enter
# Check Jenkins logs for exact error
sudo journalctl -u jenkins -n 50 --no-pager
sudo cat /var/log/jenkins/jenkins.log | tail -50
# Restart Jenkins
sudo systemctl restart jenkins
sudo systemctl status jenkinsError:
ERROR: Unable to find Jenkinsfile from git https://github.com/shubhmate/flask-express-cicd-jenkins-github-webhooks-aws-ec2
Finished: FAILURE
Cause: Both Jenkinsfiles are inside subfolders, not at the repo root. Jenkins looks at root by default.
Fix: Update the Script Path in each Jenkins job:
- Go to Jenkins → click the job → Configure
- Scroll to Pipeline section
- Change Script Path:
- For
flask-backendjob →flask-backend/Jenkinsfile - For
express-frontendjob →express-frontend/Jenkinsfile
- For
- Click Save and rebuild
Error:
ERROR: Error fetching remote repo 'origin'
Finished: FAILURE
Cause: Jenkins workspace has a stale or conflicting git state from a previous failed clone.
Fix: Clean the Jenkins workspace and rebuild:
- Go to Jenkins → click the job → Workspace (left sidebar)
- Click Wipe out current workspace
- Click Build Now to trigger a fresh build
If it still fails, verify the repo is reachable from EC2:
curl -I https://github.com/shubhmate/flask-express-cicd-jenkins-github-webhooks-aws-ec2
# Should return: HTTP/2 200Cause: Jenkins service not running or port 8080 not open in EC2 security group.
Fix:
# Check if Jenkins is running
sudo systemctl status jenkins
# If not running, start it
sudo systemctl start jenkins
# Check if port 8080 is listening
sudo ss -tlnp | grep 8080Also verify port 8080 is added to your EC2 Security Group inbound rules.
Cause: Jenkins runs as the jenkins user which doesn't have pm2 in its PATH.
Fix:
# Find where pm2 is installed
which pm2
# Use the full path in your Jenkinsfile, e.g:
# sudo -u ubuntu /usr/bin/pm2 restart flask-backend
# Add jenkins to ubuntu group and allow sudo
sudo usermod -aG ubuntu jenkins
sudo visudo
# Add: jenkins ALL=(ubuntu) NOPASSWD: /usr/bin/pm2
sudo systemctl restart jenkinsSymptom: Jenkins pipeline succeeds and pm2 list inside the pipeline shows apps online, but pm2 list as the ubuntu user shows nothing, and the app is not accessible in the browser.
Cause: Jenkins runs as the jenkins user, which has its own separate pm2 daemon at /var/lib/jenkins/.pm2. Apps started by Jenkins are invisible to the ubuntu user's pm2 daemon and may not bind to the correct network interface.
Fix: Use sudo -u ubuntu pm2 in your Jenkinsfile so apps are managed under ubuntu's pm2 daemon:
// In Jenkinsfile Deploy stage
sh 'sudo -u ubuntu pm2 restart flask-backend || sudo -u ubuntu pm2 start ${WORKSPACE}/flask-backend/venv/bin/python --name flask-backend -- ${WORKSPACE}/flask-backend/app.py'
sh 'sudo -u ubuntu pm2 save'And ensure the sudoers entry uses (ubuntu) not (ALL):
jenkins ALL=(ubuntu) NOPASSWD: /usr/bin/pm2
Error:
cd: flask-backend/flask-backend: No such file or directory
Cause: After checkout scm, the repo contents are placed directly inside WORKSPACE — not inside a subfolder named after the repo. So the path is ${WORKSPACE}/flask-backend/, not ${WORKSPACE}/flask-express-.../flask-backend/.
Fix: Use dir() blocks in your Jenkinsfile instead of cd commands:
stage('Install Dependencies') {
steps {
dir('flask-backend') {
sh 'python3 -m venv venv && venv/bin/pip install -r requirements.txt'
}
}
}Or verify the workspace structure:
# On EC2, check what's inside the Jenkins workspace
ls /var/lib/jenkins/workspace/flask-backend/
# Should show: flask-backend/ express-frontend/ README.md etc.Symptom: Jenkins UI is very slow, builds hang, or EC2 becomes unresponsive.
Cause: t2.micro has only 1GB RAM. Jenkins default JVM heap is too large.
Fix: Limit Jenkins JVM memory and add swap:
# Step 1: Limit Jenkins heap size
sudo nano /etc/default/jenkins
# Add or update this line:
JAVA_ARGS="-Xms128m -Xmx256m -XX:+UseG1GC -XX:MaxMetaspaceSize=128m"
# Step 2: Add 1GB swap file
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
# Make swap permanent across reboots
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# Step 3: Restart Jenkins
sudo systemctl restart jenkinsError:
ERROR: Unable to find Jenkinsfile from git ...
Finished: FAILURE
Cause: The Script Path in the Jenkins job config doesn't match where the Jenkinsfile actually lives in the repo.
Fix: Double-check the Script Path for each job:
| Job | Script Path |
|---|---|
flask-backend |
flask-backend/Jenkinsfile |
express-frontend |
express-frontend/Jenkinsfile |
To update: Jenkins → job → Configure → Pipeline section → Script Path → Save.
Symptom: Express app fails to start or pm2 shows status errored.
Cause: A stray } brace or other syntax error in app.js.
Fix: Check pm2 logs to find the exact error:
pm2 logs express-frontend --lines 50Then validate the syntax locally:
node --check app.js
# No output = syntax is validFix the offending line, push to GitHub, and let Jenkins redeploy.
Symptom: You push to GitHub but Jenkins does not start a build automatically.
Cause: Webhook misconfigured, Jenkins URL wrong, or EC2 security group blocking GitHub's IPs.
Fix:
-
Check webhook delivery in GitHub → Repo → Settings → Webhooks → click the webhook → Recent Deliveries. A red ❌ means delivery failed — check the response body for clues.
-
Verify the Payload URL is exactly:
http://<EC2_PUBLIC_IP>:8080/github-webhook/The trailing slash is required.
-
Confirm the Jenkins job has GitHub hook trigger for GITScm polling ticked under Build Triggers.
-
Verify port
8080is open in your EC2 Security Group inbound rules. -
Re-deliver the webhook manually: GitHub → Webhooks → Recent Deliveries → click a delivery → Redeliver.
Symptom: Jenkins pipeline succeeds but curl http://<EC2_PUBLIC_IP>:5000/ times out or refuses connection.
Cause: Flask is bound to 127.0.0.1 (localhost only) instead of 0.0.0.0 (all interfaces).
Fix: Set the FLASK_HOST environment variable before starting with pm2:
# Check current Flask binding
pm2 logs flask-backend --lines 20
# Look for: Running on http://127.0.0.1:5000 ← wrong
# Should be: Running on http://0.0.0.0:5000 ← correct
# Restart with correct host
sudo -u ubuntu pm2 delete flask-backend
sudo -u ubuntu FLASK_HOST=0.0.0.0 pm2 start venv/bin/python --name flask-backend -- app.py
sudo -u ubuntu pm2 saveOr set it in the Jenkinsfile deploy stage:
sh 'sudo -u ubuntu pm2 restart flask-backend --update-env'And ensure app.py reads the env var:
host = os.environ.get('FLASK_HOST', '0.0.0.0')
app.run(host=host, port=5000)











