Build a Jenkins CI/CD Pipeline
Dockerize a Python Flask app and automate builds with Jenkins.
Introduction
30 Second Summary
Every time a developer changes code, someone needs to check it still works, package it up, and ship it. In most companies, that someone is an automated pipeline that runs the entire process in seconds.
In this project, you will build that pipeline from scratch. You will containerize a Python app with Docker, then automate its build-test-deploy cycle using Jenkins. GitHub Copilot generates all the configuration files from your plain-English descriptions.
What You'll Build
A complete CI/CD pipeline that automatically builds, tests, and deploys a containerized Python Flask microservice using Jenkins and Docker, all developed inside GitHub Codespaces.
By the end of this project, you'll have:
- A Python Flask health-check app running inside a Docker container.
- A Jenkins server running in Docker with a fully configured pipeline that builds, tests, and deploys your app automatically.
- AI-generated configurations using GitHub Copilot to write your Dockerfile, Jenkinsfile, and Python code from natural language prompts.
- Secret Mission: A security scanning stage that checks your Docker image for vulnerabilities before deployment.
Are there any prerequisites?
You need a GitHub account (free) and basic familiarity with Linux commands like cd and ls. Everything else runs in the cloud through GitHub Codespaces, so you do not need to install Docker or Jenkins on your own machine.
Set Up Your Cloud Development Environment
Before you can build a pipeline, you need a place to write and run your code. GitHub Codespaces gives you a full Linux development environment in the cloud, accessible from your browser.
The biggest advantage for this project is that Docker comes pre-installed in every Codespace. No local setup, no RAM concerns, no compatibility headaches.
In this step, get ready to:
- Create a GitHub repository and launch a Codespace from it.
- Activate GitHub Copilot Free in the Codespaces editor.
- Verify Docker is ready to use.
Create a GitHub repository and launch Codespaces
Your GitHub repository stores all your project files and connects to your Codespace. Think of it as the home base for everything you build in this project.
- Open your browser and go to github.com.
- Sign in to your GitHub account.
- Click the + icon in the top right corner of the page.
- Select New repository.
- Enter flask-jenkins-pipeline as the Repository name.
- Leave the repository set to Public.
- Check the box for Add a README file.
- Click Create repository.
You should now see your new repository page with a single README.md file listed.
Now launch a Codespace from this repository.
- Click the green Code button on your repository page.
- Select the Codespaces tab.
- Click Create codespace on main.
A new browser tab opens with a full VS Code editor. Wait for the environment to finish loading. This may take 30-60 seconds on the first launch.
What is GitHub Codespaces?
Codespaces is a cloud-hosted development environment that runs a full Linux VM with VS Code in your browser. It comes with Docker, Git, and common developer tools pre-installed.
The free tier gives you 120 core-hours per month (60 real hours on a 2-core machine) plus 15 GB of storage. That is more than enough for this project.
Codespace not loading?
If the Codespace hangs on "Setting up your codespace", try refreshing the browser tab. If that does not work, go back to your repository page, click Code > Codespaces, delete the stuck Codespace, and create a new one.
help me troubleshoot my Codespace not loading
Activate GitHub Copilot Free in Codespaces
GitHub Copilot is an AI coding assistant that generates code from natural language descriptions. You will use it throughout this project to generate your Dockerfile, Jenkinsfile, and Python code.
- In the Codespaces VS Code editor, click the Extensions icon in the left sidebar (it looks like four squares).
- Type GitHub Copilot in the search bar.
- Click Install on the GitHub Copilot extension (published by GitHub).
- A pop-up appears asking you to sign in. Click Allow to authorize Copilot with your GitHub account.
After authorization, you should see a Copilot icon in the bottom right status bar of VS Code. This confirms Copilot is active and ready to assist.
What does Copilot Free include?
Copilot Free gives you 2,000 inline code completions and 50 chat requests per month. No credit card or payment is required. This is more than enough for this project.
Don't see the sign-in pop-up?
If no authorization pop-up appears after installing, try reloading the Codespaces window. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Developer: Reload Window, and press Enter.
help me activate GitHub Copilot in Codespaces
Verify Docker is available
Docker is the tool you will use to containerize your Flask app and run Jenkins. It comes pre-installed in Codespaces, so you just need to confirm it is working.
- Open the terminal in Codespaces by pressing Ctrl+` (backtick key, usually above Tab).
- Check that Docker is installed by running this command:
docker --version
You should see output showing a Docker version number, something like Docker version 24.x.x.
- Also verify Docker Compose is available by running:
docker compose version
You should see a Docker Compose version number in the output. Both commands returning version numbers confirms your environment is ready.
Seeing 'command not found'?
If docker is not found, your Codespace may still be initializing. Wait 30 seconds and try the command again. If it still fails, delete this Codespace and create a new one from your repository.
help me fix Docker not working in Codespaces
Your cloud development environment is ready. Next up, you will use Copilot to generate a Python Flask app from a simple English description.
Create the Python Flask App
Your cloud development environment is ready with Docker and Copilot both available. Now it's time to build the application that your CI/CD pipeline will automate.
You'll create a Python Flask microservice with a health-check endpoint. Health-check endpoints are a standard pattern in production systems where monitoring tools ping a URL to confirm a service is alive.
In this step, get ready to:
- Use GitHub Copilot Chat to generate a Flask health-check app.
- Create a requirements file and automated tests for the app.
- Run the app locally and verify it responds on port 5000.
Use Copilot Chat to generate the Flask app
Flask is a lightweight Python web framework perfect for building microservices. Your app will have two endpoints: a root / that returns a welcome message, and a /health endpoint that returns the service status.
- Open Copilot Chat by clicking the chat icon in the left sidebar of VS Code, or press Ctrl+Alt+I.
- Type the following prompt into the chat panel:
Create a Python Flask app in a file called app.py with two routes: a root route "/" that returns a JSON welcome message saying "Welcome to the Flask Health Check App", and a "/health" route that returns JSON {"status": "healthy"} with a 200 status code. Use jsonify for responses. The app should run on host 0.0.0.0 port 5000 when executed directly with a __main__ guard.
Copilot output may vary
Copilot generates code based on your prompt, but the exact output may differ slightly each time. Review the code it provides and adjust it to match the reference below. The important parts are the route paths, the JSON responses, and the host/port configuration.
- Create a new file called app.py by clicking the New File icon next to your repository name in the Explorer sidebar.
- Copy the generated code from Copilot into app.py. Make sure your file matches the following code:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({"message": "Welcome to the Flask Health Check App"})
@app.route("/health")
def health():
return jsonify({"status": "healthy"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
- from flask import Flask, jsonify imports the Flask framework and its JSON response helper.
- app = Flask(__name__) creates your Flask application instance.
- @app.route("/") and @app.route("/health") define what happens when someone visits each URL path.
- host="0.0.0.0" makes the app accessible from outside the container. This is required for Docker to forward traffic to the app.
- Save app.py with Ctrl+S.
- Confirm app.py appears in the Explorer sidebar on the left.
Copilot gave completely different code?
If Copilot's output looks very different, you can copy the reference code above directly into your file. Copilot is a productivity tool, not a requirement. The important thing is that your final code matches the reference.
help me fix my app.py to match the Flask health-check app structure
Create the requirements file and test suite
Every Python project needs a requirements.txt to declare its dependencies with pinned versions. You'll also create automated tests so your pipeline can verify the app works every time it builds.
- Create a new file called requirements.txt and add the following dependencies:
flask==3.1.0
pytest==8.3.4
Why pin version numbers?
Pinning versions (flask==3.1.0 instead of just flask) ensures your app builds identically every time. Without pins, a new library release could break your pipeline tomorrow.
- Save requirements.txt.
Now you'll create a test file that validates both endpoints return the correct responses. pytest is the testing framework that will run these checks inside your pipeline later.
- In Copilot Chat, type the following prompt:
Create a test_app.py file using pytest that tests my Flask app. Include a client fixture that uses app.test_client() with TESTING set to True. Write test_home() to check the root route returns 200 and the correct welcome message, and test_health() to check the /health route returns 200 with status "healthy".
- Create a new file called test_app.py and paste the generated code. Make sure it matches the following:
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_home(client):
response = client.get("/")
assert response.status_code == 200
assert response.get_json()["message"] == "Welcome to the Flask Health Check App"
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json()["status"] == "healthy"
What does this test file do?
- @pytest.fixture creates a reusable test client that simulates HTTP requests to your Flask app without starting a real server.
- test_home() sends a GET request to / and checks that it returns a 200 status with the correct welcome message.
- test_health() sends a GET request to /health and verifies the response contains {"status": "healthy"}.
- Save test_app.py.
- Confirm you now have three files in the Explorer sidebar: app.py, requirements.txt, and test_app.py.
Don't see all three files?
Make sure all files are saved in the root of your repository, not inside a subfolder. They should appear at the top level of the Explorer sidebar, alongside any existing files like README.md.
help me check my project file structure
✔️ Awesome, I've got everything!
Great. Make sure you've saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({"message": "Welcome to the Flask Health Check App"})
@app.route("/health")
def health():
return jsonify({"status": "healthy"}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Here is the complete requirements.txt:
flask==3.1.0
pytest==8.3.4
Here is the complete test_app.py:
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_home(client):
response = client.get("/")
assert response.status_code == 200
assert response.get_json()["message"] == "Welcome to the Flask Health Check App"
def test_health(client):
response = client.get("/health")
assert response.status_code == 200
assert response.get_json()["status"] == "healthy"
Install dependencies and run the app
With all three files in place, you can now install the dependencies and run the Flask app to confirm everything works before containerizing it.
- Install the project dependencies by running this command in the terminal:
pip install -r requirements.txt
- You should see output ending with Successfully installed flask-3.1.0 (along with its sub-dependencies) and pytest-8.3.4.
Seeing a permission error?
In Codespaces, pip should work without sudo. If you see a permissions error, make sure you're running the command in the Codespaces terminal (the panel at the bottom of VS Code), not in an external terminal.
help me fix pip install permissions in Codespaces
- Start the Flask app by running:
python app.py
- You should see output like * Running on http://0.0.0.0:5000 in the terminal.
- Codespaces automatically detects the port and shows a notification. Click Open in Browser in the notification popup, or click the PORTS tab in the bottom panel and click the globe icon next to port 5000.
Your browser opens to the forwarded URL. You should see the JSON response: {"message": "Welcome to the Flask Health Check App"}.
- Add /health to the end of the URL in your browser's address bar and press Enter.
- You should see {"status": "healthy"}.
- Go back to the Codespaces terminal and press Ctrl+C to stop the Flask app.
Don't see the port notification?
Click the PORTS tab in the bottom panel of VS Code. Look for port 5000 in the list. If it's there, click the globe icon to open it in your browser. If port 5000 is missing, make sure python app.py is still running in the terminal.
help me access my Flask app through Codespaces port forwarding
Your Flask app is working and responding to HTTP requests. Next up, you'll package it into a Docker container so it can run identically on any machine.
Dockerize the Flask App
Your Flask app works locally in Codespaces. But "works on my machine" is the oldest problem in software delivery.
Containerizing your app with Docker packages it with all its dependencies into a single portable unit. That container runs identically everywhere, whether it is your laptop, a CI server, or production.
In this step, get ready to:
- Generate a production-ready Dockerfile using GitHub Copilot.
- Build and run your Flask app as a Docker container.
- Create a .dockerignore file to keep your image clean.
Use Copilot to generate the Dockerfile
A Dockerfile is a recipe that tells Docker how to build your application image. It specifies the base operating system, installs dependencies, copies your code, and defines how to start the app.
- Open GitHub Copilot Chat by clicking the chat icon in the left sidebar of your Codespaces editor.
- Type the following prompt into the chat and press Enter:
Create a production-ready Dockerfile for my Flask app. Use python:3.12-slim as the base image. Copy requirements.txt first and install dependencies separately for layer caching. Then copy the rest of the app. Expose port 5000 and run with python app.py.
- Copilot generates a Dockerfile. Create a new file called Dockerfile in the root of your project (same folder as app.py).
- Paste or accept Copilot's output so your Dockerfile looks like this:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
What does this Dockerfile do?
- FROM python:3.12-slim starts from a lightweight Python image. The slim variant keeps the image small by excluding build tools you do not need.
- COPY requirements.txt . copies just the dependency list first. Docker caches this layer, so rebuilds skip the install step if dependencies have not changed.
- COPY . . copies the rest of your application code into the container.
- CMD ["python", "app.py"] tells Docker what command to run when the container starts.
- Save Dockerfile.
Copilot gave you something different?
AI-generated code varies between sessions. If Copilot's output does not match exactly, replace the content in your Dockerfile with the code shown above. The layer-caching order matters for build performance.
Build and run the Docker image
With the Dockerfile in place, you can now build an image and spin up a running container.
- If your Flask app is still running from the previous step, stop it by pressing Ctrl+C in that terminal.
- Build your Docker image by running this command in the terminal:
docker build -t flask-health-app .
What does this command do?
- docker build reads your Dockerfile and builds an image from it.
- -t flask-health-app tags (names) the image so you can reference it later.
- . tells Docker to use the current directory as the build context.
The build takes 30-60 seconds as Docker downloads the Python base image and installs your dependencies. You should see each step complete with a green checkmark or arrow.
Build failed?
- Make sure you are in the same directory as your Dockerfile. Run ls and confirm you see Dockerfile, app.py, and requirements.txt.
- Check that your Dockerfile has no typos. The file must be named exactly Dockerfile with a capital D and no file extension.
Help me debug my Docker build error
- Run your containerized app by executing this command:
docker run -d -p 5000:5000 flask-health-app
What does this command do?
- -d runs the container in the background (detached mode).
- -p 5000:5000 maps port 5000 on your Codespace to port 5000 inside the container.
- flask-health-app is the image name you tagged in the build step.
- Verify the containerized app is responding by running:
curl http://localhost:5000/health
You should see {"status":"healthy"} in the terminal output. Your Flask app is now running inside a Docker container.
What about the browser?
Codespaces automatically forwards port 5000. Click the PORTS tab at the bottom of your editor to see the forwarded URL. Clicking it opens the app in your browser, showing the JSON response from your containerized app.
Seeing "connection refused" or no response?
- If you get a port conflict error, your Flask app from the previous step may still be running. Stop it with Ctrl+C in the original terminal, then run the docker run command again.
- Check if the container is still running with docker ps. If it exited immediately, check logs with docker logs <container_id>.
Help me debug why my Docker container is not responding
Create a .dockerignore file
When Docker builds your image, it sends everything in the current directory to the Docker engine. A .dockerignore file tells Docker which files to skip. This keeps your image smaller and avoids leaking sensitive files.
- Open Copilot Chat and type this prompt:
Create a .dockerignore file for my Flask project. Exclude .git, __pycache__, .pyc files, .venv, .env, markdown files, the .dockerignore itself, and the Jenkinsfile.
- Create a new file called .dockerignore in the root of your project (same folder as Dockerfile).
- Paste or accept Copilot's output so your .dockerignore looks like this:
.git
__pycache__
*.pyc
.venv
.env
*.md
.dockerignore
Jenkinsfile
Why exclude these files?
- .git contains your entire version history. Your image does not need it.
- __pycache__ and *.pyc are compiled Python bytecode that gets regenerated inside the container.
- .env often holds secrets. Never bake secrets into a Docker image.
- Jenkinsfile is a CI/CD config. It has no purpose inside the running app container.
- Save .dockerignore.
- Confirm both new files exist by running:
ls -a
You should see Dockerfile and .dockerignore in the output alongside your existing app.py, requirements.txt, and test_app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved both Dockerfile and .dockerignore.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
And the complete .dockerignore:
.git
__pycache__
*.pyc
.venv
.env
*.md
.dockerignore
Jenkinsfile
Your Flask app is now fully containerized and running in Docker. Next up, you will launch a Jenkins server inside another container and connect it to build your app automatically.
Launch Jenkins in Docker
Your Flask app is containerized and proven to work. The next challenge is automating the build-test-deploy cycle so it runs every time you push code.
Jenkins is one of the most widely-used CI/CD automation servers in enterprise environments. You will run Jenkins itself inside a Docker container in your Codespace, which means zero permanent installation and easy cleanup.
In this step, get ready to:
- Run a Jenkins container with access to Docker.
- Complete the Jenkins initial setup wizard.
- Install the Docker Pipeline plugin.
Run the Jenkins container with Docker access
Jenkins needs two things to build Docker images from its pipeline: the Docker socket (to communicate with the Docker daemon) and the Docker CLI (to run docker commands). The official Jenkins image does not include the Docker CLI by default, so you will add it after starting the container.
- First, stop your running Flask container to free up resources by running this command:
docker stop $(docker ps -q --filter ancestor=flask-health-app)
- Start the Jenkins container by running this command:
docker run -d --name jenkins \
-p 8080:8080 \
--restart=on-failure \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \
jenkins/jenkins:lts-jdk21
What does this command do?
- -d runs the container in the background (detached mode).
- --name jenkins gives the container a memorable name so you can reference it easily.
- -p 8080:8080 maps port 8080 inside the container to port 8080 on your Codespace. This is how you will access the Jenkins web UI.
- -v jenkins_home:/var/jenkins_home persists Jenkins data (jobs, plugins, config) in a Docker volume so it survives container restarts.
- -v /var/run/docker.sock:/var/run/docker.sock mounts the host's Docker socket into the container. This lets Jenkins talk to the same Docker daemon that runs your Codespace.
- Confirm the container is running by running:
docker ps
You should see a row for jenkins with status Up and port 8080 mapped.
Container exited immediately?
Run docker logs jenkins to check the output. If you see a port conflict error, make sure no other process is using port 8080.
Help me debug why my Jenkins container won't start.
Now you need to install the Docker CLI inside the Jenkins container and grant it permission to use the socket.
- Install the Docker CLI inside the Jenkins container by running:
docker exec -u root jenkins bash -c "apt-get update && apt-get install -y docker.io"
- Grant the Jenkins user permission to access the Docker socket by running:
docker exec -u root jenkins chmod 666 /var/run/docker.sock
Why run these as root?
The Jenkins container runs its main process as the jenkins user, which does not have permission to install packages or modify file permissions. The -u root flag temporarily switches to root for these administrative commands.
The chmod 666 on the Docker socket allows any user inside the container to communicate with the Docker daemon. This is acceptable for a learning environment.
- Verify that Docker is accessible from inside Jenkins by running:
docker exec jenkins docker --version
You should see a Docker version string like Docker version 20.x.x or similar. This confirms Jenkins can run Docker commands.
Seeing permission denied?
If you get permission denied when running docker exec jenkins docker --version, re-run the chmod command above. Make sure you included -u root in that command.
Help me fix Docker socket permissions inside my Jenkins container.
Complete the Jenkins initial setup
Jenkins requires a one-time setup wizard when you first access it. You will retrieve an auto-generated admin password, unlock Jenkins, install recommended plugins, and create your admin account.
- Jenkins takes about 30-60 seconds to fully start. Wait a moment, then retrieve the initial admin password by running:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
- Copy the password string that appears in your terminal. You will paste this into the Jenkins UI in the next step.
Seeing 'No such file or directory'?
Jenkins has not finished starting yet. Wait 30 more seconds and try the command again. You can check if Jenkins is ready by running docker logs jenkins and looking for the line Jenkins is fully up and running.
- Open the Jenkins UI through Codespaces port forwarding. Click the **PORTS** tab at the bottom of your Codespaces editor.
- Find port 8080 in the list. Click the globe icon (or the forwarded URL) next to it to open Jenkins in a new browser tab.
- On the **Unlock Jenkins** page, paste the admin password you copied from the terminal.
- Click **Continue**.
What is Codespaces port forwarding?
When an application inside your Codespace listens on a port, Codespaces automatically detects it and creates a URL you can open in your browser. The URL format looks like https://CODESPACENAME-8080.app.github.dev. This means you can access Jenkins without any extra network configuration.
- On the **Customize Jenkins** page, click **Install suggested plugins**.
- Wait for the plugins to finish installing. This takes 2-3 minutes. You will see a progress screen showing each plugin being downloaded and installed.
- On the **Create First Admin User** page, fill in the following fields:
- Enter a username (e.g. admin).
- Enter a password you will remember.
- Enter your full name and email address.
- Click **Save and Continue**.
- On the **Instance Configuration** page, leave the Jenkins URL as the default value.
- Click **Save and Finish**.
- Click **Start using Jenkins**.
You should now see the Jenkins dashboard with a welcome message. Jenkins is ready to use.
Jenkins page not loading?
Make sure you are clicking the forwarded URL from the **PORTS** tab, not typing localhost:8080 in your browser. Codespaces runs in the cloud, so localhost will not work.
If the port does not appear in the PORTS tab, run docker ps to verify the Jenkins container is still running.
Help me access my Jenkins UI through Codespaces port forwarding.
Install the Docker Pipeline plugin
The Docker Pipeline plugin lets Jenkins run Docker commands directly from your Jenkinsfile pipeline steps. Without it, your pipeline cannot build or run Docker images.
- In the Jenkins dashboard, click **Manage Jenkins** in the left sidebar.
- Click **Plugins**.
- Click the **Available plugins** tab on the left.
- In the search box, type Docker Pipeline.
- Check the box next to **Docker Pipeline** in the results list.
- Click **Install** (or **Install without restart**).
- Wait for the installation to complete. You will see a success checkmark next to the plugin name.
What is the Docker Pipeline plugin?
This plugin (also known as docker-workflow) integrates Docker operations into Jenkins Pipeline scripts. It allows your Jenkinsfile to use sh 'docker build ...' commands and provides additional Docker-specific pipeline steps.
Don't see Docker Pipeline in the list?
Make sure you are on the **Available plugins** tab, not the **Installed** tab. If the list appears empty, click **Check now** to refresh the plugin index from the update center.
Help me find and install the Docker Pipeline plugin in Jenkins.
Jenkins is now fully configured with Docker access and the Docker Pipeline plugin. Next up, you will create a Jenkinsfile pipeline that automatically builds, tests, and deploys your Flask app.
Create a Jenkins Pipeline
Your Jenkins server is up and running. Now you need to give it a set of instructions that define exactly what happens when code arrives.
A Jenkinsfile is that instruction manual. It defines each stage of your CI/CD workflow as code, living right alongside your application in the same repository.
In this step, get ready to:
- Generate a Jenkinsfile with three pipeline stages using GitHub Copilot.
- Push all project files to your GitHub repository.
- Create and run a Jenkins Pipeline job that builds, tests, and deploys your app.
Use Copilot Chat to generate the Jenkinsfile
Your pipeline needs three stages. Build creates the Docker image. Test runs pytest inside the container. Deploy launches the container and verifies the health endpoint responds.
- Open GitHub Copilot Chat by clicking the chat icon in the left sidebar of your Codespaces editor.
- Send the following prompt to Copilot Chat:
Create a declarative Jenkinsfile for my Flask app with three stages:
1. Build - runs docker build -t flask-health-app .
2. Test - runs docker run --rm flask-health-app pytest test_app.py -v
3. Deploy - removes any existing container named flask-health-app-running, runs the container on port 5000, waits 3 seconds, then curls the health endpoint to verify it works.
Also add a post block with always, success, and failure echo messages.
Why write the prompt this way?
Being specific about the exact commands you want in each stage gives Copilot the best chance of generating code that works on the first try. Vague prompts like "create a Jenkins pipeline" produce generic templates you would need to heavily edit.
- Copilot generates a Jenkinsfile. Verify the output matches the code below, then create a new file called Jenkinsfile in the root of your project (no file extension) and paste the code into it:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t flask-health-app .'
}
}
stage('Test') {
steps {
sh 'docker run --rm flask-health-app pytest test_app.py -v'
}
}
stage('Deploy') {
steps {
sh 'docker rm -f flask-health-app-running || true'
sh 'docker run -d --name flask-health-app-running -p 5000:5000 flask-health-app'
sh 'sleep 3'
sh 'curl -f http://localhost:5000/health'
}
}
}
post {
always {
echo 'Pipeline execution completed.'
}
success {
echo 'All stages passed successfully!'
}
failure {
echo 'Pipeline failed. Check the logs above.'
}
}
}
What does this Jenkinsfile do?
Each stage runs a shell command inside the Jenkins agent:
- Build rebuilds your Docker image from the latest code in the workspace.
- Test spins up a temporary container and runs pytest against your test file. The --rm flag deletes the container automatically after tests finish.
- Deploy removes any previously running container, starts a fresh one on port 5000, waits 3 seconds for it to boot, then verifies the /health endpoint returns a success response.
- post block runs cleanup or notification logic regardless of whether the pipeline passed or failed.
- Save Jenkinsfile.
Copilot generated something different?
Copilot output varies between sessions. If the generated code does not match the version above, replace it entirely with the code shown. The exact commands in each stage matter for the pipeline to pass.
Help me fix my Jenkinsfile
✔️ Awesome, I've got everything!
Great. Make sure you have saved your Jenkinsfile before moving on.
ⓧ I'd like to double check the full code
Here is the complete Jenkinsfile you should have:
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'docker build -t flask-health-app .'
}
}
stage('Test') {
steps {
sh 'docker run --rm flask-health-app pytest test_app.py -v'
}
}
stage('Deploy') {
steps {
sh 'docker rm -f flask-health-app-running || true'
sh 'docker run -d --name flask-health-app-running -p 5000:5000 flask-health-app'
sh 'sleep 3'
sh 'curl -f http://localhost:5000/health'
}
}
}
post {
always {
echo 'Pipeline execution completed.'
}
success {
echo 'All stages passed successfully!'
}
failure {
echo 'Pipeline failed. Check the logs above.'
}
}
}
Push your code to GitHub
Jenkins needs to pull your code from Git. Since you created this repo through GitHub before launching Codespaces, the remote is already configured.
- Stop any Flask containers still running from the previous step by running this command:
docker stop $(docker ps -q --filter ancestor=flask-health-app) 2>/dev/null || true
- Stage, commit, and push all your project files by running these commands:
git add .
git commit -m "Add Flask app with Docker and Jenkins pipeline"
git push
What do these commands do?
- git add . stages every file in your project folder for commit.
- git commit saves a snapshot of your staged files with a message.
- git push uploads that snapshot to your GitHub repository so Jenkins can access it.
- You should see output confirming your files were pushed to the main branch on GitHub.
Push failed or permission denied?
Make sure your repository visibility is set to Public on GitHub. Jenkins needs to clone it without credentials. If you see an authentication error, Codespaces may need you to authorize Git. Run gh auth status to check.
Help me fix my git push error
Configure Jenkins to pull from your repository
Now you will create a Pipeline job in Jenkins that reads the Jenkinsfile directly from your GitHub repository.
- Open the Jenkins UI in your browser using the port 8080 forwarded URL from the PORTS tab in Codespaces.
- Click New Item in the left sidebar of the Jenkins dashboard.
- Enter flask-health-pipeline in the item name field.
- Select Pipeline from the project type list.
- Click OK at the bottom of the page.
You are now on the pipeline configuration page.
- Scroll down to the Pipeline section at the bottom.
- Change the Definition dropdown from Pipeline script to Pipeline script from SCM.
- Select Git from the SCM dropdown.
- In the Repository URL field, paste your GitHub repository URL. You can find it by opening your repository on GitHub and clicking the green Code button. Copy the HTTPS URL (it looks like https://github.com/your-username/your-repo-name.git).
- Change the Branch Specifier from */master to */main.
- Leave the Script Path field as Jenkinsfile (this is the default).
- Click Save.
Why Pipeline script from SCM?
This tells Jenkins to look for a Jenkinsfile in your repository instead of expecting you to paste the pipeline code into the Jenkins UI. This is the industry standard approach because your pipeline definition lives with your code and is version-controlled.
- Click Build Now in the left sidebar to trigger your pipeline.
Jenkins clones your repository, reads the Jenkinsfile, and executes each stage in order. You can watch the progress in the Stage View on the job page.
- Wait for all three stages (Build, Test, Deploy) to show green checkmarks.
- Click the build number (e.g. #1) in the Build History panel on the left, then click Console Output to see the full log of what happened.
What should I see in Console Output?
The log shows each stage executing in sequence. The Build stage runs docker build. The Test stage shows pytest passing both tests. The Deploy stage shows curl returning the JSON health response.
The final line reads Finished: SUCCESS.
Pipeline failed at the Deploy stage?
The most common cause is a port conflict on port 5000. If another container is already using that port, the Deploy stage cannot start a new one. Run docker ps in your Codespaces terminal to check for running containers on port 5000.
Stop them with docker stop <container-id>, then click Build Now again in Jenkins.
Help me fix my pipeline Deploy stage failure
Your CI/CD pipeline is fully operational. You now have Jenkins automatically building, testing, and deploying your Dockerized Flask app from code stored in GitHub.
Secret mission
Add a Security Scan Stage with Trivy
Your pipeline builds, tests, and deploys. But in a real DevOps team, no image ships to production without a security scan. Can you add a vulnerability scanner to your pipeline that blocks deployments when critical issues are found?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs in GitHub Codespaces, which has a free tier of 120 core-hours per month.
Cost warning
GitHub Codespaces provides 120 core-hours per month for free (60 real hours on a 2-core machine). If you leave your Codespace running continuously, you could exhaust your free hours within a few days.
Codespaces automatically stop after 30 minutes of inactivity, but it is best to stop yours manually when you finish working.
Resources you used:
- GitHub Codespace (cloud development environment with Docker pre-installed).
- GitHub repository (stores your app code, Dockerfile, and Jenkinsfile).
- Docker containers (Jenkins server and Flask app running inside your Codespace).
- Docker images (flask-health-app and jenkins/jenkins:lts-jdk21 pulled/built in your Codespace).
- GitHub Copilot Free (2,000 completions + 50 chat requests per month, no payment required).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue with Project 2 soon.
- Your Codespace stays active and will auto-stop after 30 minutes of inactivity.
- Your Jenkins container and Flask container keep running inside the Codespace.
- Your GitHub repository remains available at any time.
Pause - I'll come back to this later
Stop your Codespace to preserve free hours. Your files and containers are saved and will resume when you restart.
- Go to github.com/codespaces in your browser.
- Find your Codespace in the list and click the three-dot menu on the right.
- Select Stop codespace.
- When you are ready to resume, click the same Codespace to restart it. Your Docker containers will need to be restarted manually.
Delete - I don't want to use this again
Remove your Codespace entirely. Your GitHub repository and code remain safe on GitHub.
- Go to github.com/codespaces in your browser.
- Find your Codespace in the list and click the three-dot menu on the right.
- Select Delete to permanently remove the Codespace and all Docker containers and images inside it.
- If you also want to delete the GitHub repository, go to your repository page, click Settings, scroll to the bottom, and click Delete this repository.
That's a Wrap!
That's a Wrap!
Nice work! You just built a real DevOps pipeline from scratch using AI assistance. You now have a working Jenkins CI/CD pipeline that automatically builds, tests, and deploys a containerized Python app.
You've learned how to:
- Created a Python Flask microservice and containerized it with a production-ready Dockerfile.
- Ran Jenkins inside Docker and configured it from scratch, including plugins and pipeline jobs.
- Built a declarative Jenkinsfile pipeline with Build, Test, and Deploy stages using GitHub Copilot to generate configurations from natural language.
- Secret Mission: Added container security scanning with Trivy to catch vulnerabilities before deployment.
Ready to quiz yourself?