A learning-in-public story about Flask, Jenkins, AWS EC2, systemd, and finally shipping a live demo on Vercel. 🎯 TL;DR I built PyPulse, a tiny Flask app, and wired it up to a full CI/CD pipeline: push to GitHub → Jenkins builds → tests → deploys to AWS EC2 → auto-triggered via webhook → managed by systemd.
Along the way I broke almost every piece of it at least once, and fixed each one.
I also deployed a permanent live demo on Vercel, since my EC2 instance is running on the AWS free trial and won't live forever. 🔗 Live demo: pypulse-pi.vercel.app 🔗 Live demo (health check): pypulse-pi.vercel.app/health If you're learning DevOps and want to see what the real, messy version of "just set up a CI/CD pipeline" looks like — not the polished tutorial version — this is that. 🧰 The Stack Piece Tool Job App Flask + pytest + gunicorn The actual web app and its tests CI/CD Jenkins (on EC2, Ubuntu 22.04) Build → Test → Deploy automation Source control GitHub Single source of truth Trigger GitHub Webhook Auto-runs the pipeline on every push Process management systemd Keeps the app alive on reboot/crash Permanent demo Vercel Live URL that survives EC2 termination 🏗️ The App: PyPulse Nothing fancy on purpose — the whole point of this project was the pipeline, not the app. python app.py from flask import Flask, jsonify from datetime import datetime, timezone app = Flask(name) @app.route("/") def home(): return jsonify({ "message": "Hello from PyPulse", "time": datetime.now(timezone.utc).isoformat() }) @app.route("/health") def health(): return jsonify({"status": "ok"}), 200 if name == "main": app.run(host="0.0.0.0", port=5000) Two routes.
Two tests.
That's it.
Small enough that when something broke, I knew it wasn't the app — it was the plumbing around it.
That turned out to be the right call, because the plumbing broke a lot. 😅 ⚙️ The Pipeline: Build → Test → Deploy Here's the mental model I ended up with for a Jenkinsfile: Each stage is a gate.
If Build fails, Test never runs.
If Test fails, Deploy never runs.
That ordering is the entire value of CI/CD — automation with checkpoints, not just automation. 🔨 Build groovy stage('Build') { steps { sh ''' python3 -m venv venv . venv/bin/activate pip install -r requirements.txt ''' } } Runs on the Jenkins machine itself.
Fresh virtual environment, install dependencies.
If a dependency is broken or missing, this fails immediately — before wasting time testing or deploying broken code. ✅ Test groovy stage('Test') { steps { sh ''' . venv/bin/activate pytest ''' } } Runs the two tests against / and /health.
This is the safety net: if I break a route later, this stage fails and the pipeline stops before broken code ever reaches the live server. 🚀 Deploy groovy stage('Deploy') { steps { sshagent(credentials: ['pypulse-ec2-ssh']) { sh ''' set -e timeout 60 ssh -o StrictHostKeyChecking=no ${EC2_USER}@${EC2_IP} " set -e if [ ! -d ${APP_DIR} ]; then git clone https://github.com/IrfanPasha05/pypulse.git ${APP_DIR} fi cd ${APP_DIR} git pull origin main source venv/bin/activate pip install -r requirements.txt sudo systemctl restart pypulse " ''' } } } Only runs if Build and Test both passed.
SSHes into EC2, pulls the latest code, reinstalls dependencies, and restarts a systemd service (more on why, below). 🐛 The Debugging Journey (the actually useful part) Anyone can copy a working Jenkinsfile.
Here's what happened when mine didn't work — because this is the part that actually teaches you something.
Bug #1: "ensurepip is not available" The virtual environment was not created successfully because ensurepip is not available.
On Debian/Ubuntu systems, you need to install the python3-venv package What happened: Jenkins runs on the same machine as the OS, and that machine didn't have the python3-venv package installed for its Python version.
Fix: bash sudo apt install python3.14-venv -y Lesson: the Jenkins host machine needs every tool your build script assumes exists.
It's easy to forget Jenkins isn't magi