Build a CI/CD Pipeline with Jenkins

Set up Jenkins in Docker and build a multi-stage CI/CD pipeline.

Introduction

30 Second Summary

Every time a developer pushes code to a team project, someone has to check if it still works. Without automation, that means manually running tests, building the app, and hoping nothing breaks before it reaches users.

In this project, you will build a CI/CD pipeline using Jenkins that automatically builds, tests, and packages a Python application every time code changes.

What You'll Build

A local Jenkins server running in Docker that executes a multi-stage pipeline against a Python calculator application.

By the end of this project, you'll have:

  • A running Jenkins server accessible at http://localhost:8080 with full Docker-in-Docker support.
  • A Python application with automated pytest unit tests that validate calculator functions.
  • A Declarative Pipeline (Jenkinsfile) defining Build, Test, and Package stages that run inside an isolated Docker container.
  • Secret Mission: Add a code quality gate with a linter that enforces code standards and fails the build if quality drops.

Are there any prerequisites?

This project assumes you have Docker Desktop installed and running on your machine. You should also be comfortable using Git and the command line for basic tasks like navigating folders and running commands.

Set Up Jenkins with Docker

Every time a developer pushes code, someone needs to verify it actually works. Building, testing, and packaging code by hand is slow and error-prone.

Jenkins is an automation server that handles all of this for you. In this step, you will get Jenkins running locally inside Docker so it is completely isolated from the rest of your system.

In this step, get ready to:
  • Launch a Docker-in-Docker container that lets Jenkins run Docker commands inside pipelines.
  • Build a custom Jenkins Docker image with the plugins you need.
  • Run Jenkins and complete the setup wizard.
Create a Docker network and start the Docker-in-Docker container

Jenkins needs to run Docker commands inside its pipelines. To make this work, you will run a separate Docker-in-Docker (dind) container that acts as a Docker engine for Jenkins to talk to.

Both containers need to communicate over a shared network. You will create a dedicated bridge network for this purpose.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Create the bridge network by running this command:
docker network create jenkins

What is a bridge network?

A bridge network is a private internal network that Docker containers use to talk to each other. By creating a dedicated network named jenkins, you ensure the Jenkins container and the Docker-in-Docker container can find each other by name.

  • Start the Docker-in-Docker container by running this command:

macOS

docker run --name jenkins-docker --rm --detach \
  --privileged --network jenkins --network-alias docker \
  --env DOCKER_TLS_CERTDIR=/certs \
  --volume jenkins-docker-certs:/certs/client \
  --volume jenkins-data:/var/jenkins_home \
  --publish 2376:2376 \
  docker:dind --storage-driver overlay2

Windows

docker run --name jenkins-docker --rm --detach ^
  --privileged --network jenkins --network-alias docker ^
  --env DOCKER_TLS_CERTDIR=/certs ^
  --volume jenkins-docker-certs:/certs/client ^
  --volume jenkins-data:/var/jenkins_home ^
  --publish 2376:2376 ^
  docker:dind

What does this command do?

  • --privileged gives this container the elevated permissions needed to run a Docker engine inside Docker.
  • --network jenkins --network-alias docker connects the container to the jenkins network and makes it reachable at the hostname docker.
  • --volume jenkins-data:/var/jenkins_home shares a volume so Jenkins can access files created by this Docker engine.
  • --volume jenkins-docker-certs:/certs/client shares TLS certificates so the connection between the two containers is encrypted.
  • Confirm the container is running by running:
docker ps

You should see a container named jenkins-docker with the docker:dind image listed in the output.

Don't see the container?

  • Make sure Docker Desktop is running. Check for the Docker icon in your system tray or menu bar.
  • On Windows, Docker Desktop must be configured to run Linux Containers, not Windows Containers. Right-click the Docker icon in your system tray and check for a Switch to Linux containers option.
  • Help me troubleshoot my Docker-in-Docker container
Create a custom Jenkins Dockerfile and build the image

The official Jenkins image does not include the Docker CLI or the pipeline plugins you need. You will create a custom Dockerfile that adds both.

  • Create a new directory for your Dockerfile and move into it by running these commands:
mkdir jenkins-docker-setup
cd jenkins-docker-setup
  • Create a file named Dockerfile in this directory. Open it in your text editor and paste the following content:
FROM jenkins/jenkins:2.555.3-jdk21
USER root
RUN apt-get update && apt-get install -y lsb-release
RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
  https://download.docker.com/linux/debian/gpg
RUN echo "deb [arch=$(dpkg --print-architecture) \
  signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
  https://download.docker.com/linux/debian \
  $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
RUN apt-get update && apt-get install -y docker-ce-cli
USER jenkins
RUN jenkins-plugin-cli --plugins "blueocean docker-workflow json-path-api"

What does this Dockerfile do?

  • FROM jenkins/jenkins:2.555.3-jdk21 starts from the official Jenkins LTS image with Java 21.
  • The middle RUN commands install the Docker CLI so Jenkins can talk to the Docker-in-Docker engine you started earlier.
  • jenkins-plugin-cli --plugins "blueocean docker-workflow json-path-api" pre-installs the plugins needed for Docker-based pipelines.
  • Save the Dockerfile.
  • Build the custom image by running this command:
docker build -t myjenkins:2.555.3-1 .

This takes a few minutes on the first run because Docker needs to download the base Jenkins image and install packages.

  • Confirm the image was built by running:
docker images

You should see myjenkins with the tag 2.555.3-1 in the list.

Build failed with an error?

  • Check that your Dockerfile has no typos. The most common mistake is a missing backslash (\) at the end of multi-line RUN commands.
  • Make sure you are running the docker build command from the same directory where your Dockerfile is saved. The . at the end of the command refers to the current directory.
  • Help me fix my Jenkins Docker build error
Run the Jenkins container and complete the setup wizard

Now you will start the actual Jenkins server using your custom image. Once it boots up, you will walk through the one-time setup wizard to get Jenkins ready.

  • Start the Jenkins container by running this command:

macOS

docker run --name jenkins-blueocean --restart=on-failure --detach \
  --network jenkins --env DOCKER_HOST=tcp://docker:2376 \
  --env DOCKER_CERT_PATH=/certs/client --env DOCKER_TLS_VERIFY=1 \
  --publish 8080:8080 --publish 50000:50000 \
  --volume jenkins-data:/var/jenkins_home \
  --volume jenkins-docker-certs:/certs/client:ro \
  myjenkins:2.555.3-1

Windows

docker run --name jenkins-blueocean --restart=on-failure --detach ^
  --network jenkins --env DOCKER_HOST=tcp://docker:2376 ^
  --env DOCKER_CERT_PATH=/certs/client --env DOCKER_TLS_VERIFY=1 ^
  --volume jenkins-data:/var/jenkins_home ^
  --volume jenkins-docker-certs:/certs/client:ro ^
  --publish 8080:8080 --publish 50000:50000 myjenkins:2.555.3-1

What does this command do?

  • --env DOCKER_HOST=tcp://docker:2376 tells Jenkins where to find the Docker engine (the dind container you started earlier, reachable at hostname docker).
  • --publish 8080:8080 maps port 8080 so you can access Jenkins in your browser.
  • --volume jenkins-data:/var/jenkins_home persists all Jenkins data (jobs, plugins, settings) even if the container restarts.
  • --volume jenkins-docker-certs:/certs/client:ro gives Jenkins read-only access to the TLS certificates needed to talk to the Docker engine securely.
  • Retrieve the initial admin password by running:
docker exec jenkins-blueocean cat /var/jenkins_home/secrets/initialAdminPassword

You should see a long string of letters and numbers printed in your terminal. Copy this password.

Why do I need this password?

Jenkins generates a one-time password on first boot to prevent unauthorized access. You will paste it into the setup wizard to prove you are the server administrator.

  • Open your browser and go to http://localhost:8080.
  • Wait for the Unlock Jenkins page to appear.
  • Paste the password you copied into the Administrator password field.
  • Click Continue.

Give Jenkins a moment to boot

If you see a connection error when you first open http://localhost:8080, wait 30-60 seconds and refresh. Jenkins takes a moment to start up inside the container.

  • On the Customize Jenkins page, click Install suggested plugins.
  • Wait for the plugins to finish installing. This takes a few minutes.
  • On the Create First Admin User page, fill in a username, password, and full name of your choice.
  • Click Save and Finish.
  • On the Jenkins is ready page, click Start using Jenkins.

You should now see the Jenkins dashboard in your browser. This is your CI/CD command center.

Stuck on the setup wizard?

  • If the page says Jenkins is almost ready! instead of Jenkins is ready, click Restart and wait a minute for Jenkins to reload.
  • If the page does not refresh automatically, manually refresh your browser after a minute.
  • Help me get past the Jenkins setup wizard

Your Jenkins server is live and ready to run pipelines. Next up, you will create the Python application that your pipeline will build, test, and package.

Create the Python Application

Your Jenkins server is up and running. Now you need an application for it to build, test, and package.

In this step, you'll create a simple Python calculator app with automated tests. This gives your pipeline something real to work with when you write the Jenkinsfile in the next step.

In this step, get ready to:
  • Create the project file structure with directories for source code and tests.
  • Write a calculator module and its pytest test suite.
  • Initialize a Git repository with all files committed.
Create the project structure

Every CI/CD pipeline needs source code to operate on. You'll create a project folder on your Desktop with separate directories for the application code and its tests.

Windows users

Use Git Bash for all terminal commands in this project. Git Bash provides the same commands as macOS/Linux terminals. You installed it alongside Git.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir python-calculator
cd python-calculator
  • Create the directory structure and placeholder files by running these commands:
mkdir app tests
touch app/calculator.py tests/test_calculator.py requirements.txt Jenkinsfile
  • Confirm the files exist by running:
ls app tests

You should see calculator.py listed under app/ and test_calculator.py listed under tests/.

Not seeing the files?

Make sure you ran cd python-calculator before creating the directories. If you're on Windows, confirm you're using Git Bash and not Command Prompt.

Help me troubleshoot my project structure

Write the calculator app and tests

Now you'll fill in the application code and its test suite. The calculator module provides four basic math functions. The test file uses pytest to verify each function works correctly, including edge cases.

  • Open the python-calculator folder in your text editor by running:
code .

Using a different editor?

If you use Cursor instead of VS Code, run cursor . instead. You can also open the folder manually through your editor's File menu.

  • Open app/calculator.py in the editor and paste the following code:
def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def multiply(a, b):
    return a * b


def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

What does this code do?

This module defines four arithmetic functions: add, subtract, multiply, and divide.

The divide function includes a guard that raises a ValueError when someone tries to divide by zero. This gives the test suite an edge case to verify.

  • Save app/calculator.py.
  • You won't see a visible change yet. The test file you write next will exercise these functions.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def multiply(a, b):
    return a * b


def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b
  • Open tests/test_calculator.py in the editor and paste the following code:
import pytest
from app.calculator import add, subtract, multiply, divide


def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0


def test_subtract():
    assert subtract(5, 3) == 2
    assert subtract(1, 1) == 0
    assert subtract(0, 5) == -5


def test_multiply():
    assert multiply(3, 4) == 12
    assert multiply(-2, 3) == -6
    assert multiply(0, 100) == 0


def test_divide():
    assert divide(10, 2) == 5.0
    assert divide(7, 2) == 3.5
    assert divide(-6, 3) == -2.0


def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)

What does this test file do?

Each function starting with test_ is automatically discovered by pytest. The assert statements check that each calculator function returns the expected result.

The test_divide_by_zero function uses pytest.raises to confirm that dividing by zero raises a ValueError with the correct message. This is how you test that error handling works properly.

  • Save tests/test_calculator.py.

✔️ Awesome, I've got everything!

Great. Make sure you've saved the file before moving on.

ⓧ I'd like to double check the full code

import pytest
from app.calculator import add, subtract, multiply, divide


def test_add():
    assert add(2, 3) == 5
    assert add(-1, 1) == 0
    assert add(0, 0) == 0


def test_subtract():
    assert subtract(5, 3) == 2
    assert subtract(1, 1) == 0
    assert subtract(0, 5) == -5


def test_multiply():
    assert multiply(3, 4) == 12
    assert multiply(-2, 3) == -6
    assert multiply(0, 100) == 0


def test_divide():
    assert divide(10, 2) == 5.0
    assert divide(7, 2) == 3.5
    assert divide(-6, 3) == -2.0


def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero"):
        divide(10, 0)
  • Open requirements.txt in the editor and add this single line:
pytest==9.1.1

Why pin the version?

Pinning pytest==9.1.1 ensures every pipeline run uses the exact same version. Without a pin, a future pytest release could change behavior and break your build unexpectedly.

  • Save requirements.txt.

Seeing import errors in your editor?

If you see red underlines in your editor for the from app.calculator import line, that's expected. Your editor may not recognize the import path without a local Python environment. The Jenkins pipeline will handle this inside a Docker container.

Why is my editor showing import errors?

Initialize a Git repository

Jenkins reads source code from a Git repository. You'll initialize one in your project folder so Jenkins can pull from it later.

  • Initialize a new Git repository by running this command (make sure you're inside the python-calculator folder):
git init
  • Stage all files and create your initial commit by running these commands:
git add .
git commit -m "Initial commit: calculator app with tests"

What does this do?

The git add . command stages every file in the project. The git commit command saves a snapshot of those files. Jenkins will use this snapshot as the source for your pipeline.

  • Verify the commit by running:
git log --oneline

You should see one line showing your commit message: Initial commit: calculator app with tests.

Git asking for your name and email?

If Git prompts you to set your identity before committing, run these two commands with your own details:

git config user.email "you@example.com"
git config user.name "Your Name"

Then run the git commit command again.

Help me set up my Git identity

Your Python application is ready. You have source code, tests, a pinned dependency, and everything committed to Git. Next up, you'll write the Jenkinsfile that tells Jenkins how to build, test, and package this application automatically.

Write the Jenkinsfile

Your Python app and tests are ready. Now you need to tell Jenkins exactly how to build, test, and package your application automatically.

You'll do this by writing a Jenkinsfile. This is the Pipeline-as-Code approach where your entire build process lives in a file checked into version control, right alongside your source code.

In this step, get ready to:
  • Define a Declarative Pipeline with a Docker agent.
  • Create Build, Test, and Package stages.
  • Add automated test result reporting.
Define the Declarative Pipeline structure with a Docker agent

A Declarative Pipeline defines your entire CI/CD process in a structured, readable format. Instead of clicking through a UI to configure jobs, everything is defined in code.

The Docker agent tells Jenkins to spin up a fresh Python container for every build. Each run starts clean with no leftover files from previous builds.

Why Declarative over Scripted Pipeline?

Jenkins supports two pipeline styles. Declarative is the recommended approach for all new pipelines. It is structured, easier to read, and has built-in error handling via the post section.

Scripted Pipeline gives full Groovy access but is harder to maintain. Use it only when you need advanced logic that Declarative cannot express.

  • Open Jenkinsfile in your text editor and replace its placeholder content with the following pipeline definition:
pipeline {
    agent {
        docker {
            // Spin up a lightweight Python container for every build
            image 'python:3.14.6-alpine3.24'
        }
    }

    stages {
        stage('Build') {
            steps {
                // Install project dependencies from requirements.txt
                sh 'pip install -r requirements.txt'
            }
        }

        stage('Test') {
            steps {
                // Run pytest and output results in JUnit XML format
                sh 'python -m pytest tests/ --verbose --junit-xml=results.xml'
            }
        }

        stage('Package') {
            steps {
                // Create a distributable archive of the app
                sh 'tar -czf calculator-app.tar.gz app/'
                // Store the archive in Jenkins for download
                archiveArtifacts artifacts: 'calculator-app.tar.gz', fingerprint: true
            }
        }
    }

    post {
        always {
            // Publish test results to Jenkins regardless of pass/fail
            junit 'results.xml'
        }
    }
}

What does the Docker agent do?

  • agent { docker { image 'python:3.14.6-alpine3.24' } } tells Jenkins to pull this Python image and run every stage inside it.
  • Each build gets a brand new container. Nothing persists between runs, so you always start from a known-good state.
  • The alpine variant is small and fast to pull, keeping your pipeline quick.
  • Save Jenkinsfile.

File not saving?

  • Make sure the file is named exactly Jenkinsfile with a capital J and no file extension.
  • Confirm you are editing the file inside your project directory (the same folder that contains app/ and tests/).
  • help me figure out why my Jenkinsfile isn't saving correctly
Create the Build, Test, and Package stages

The stages block you just wrote defines three sequential steps that Jenkins runs in order. If any stage fails, the pipeline stops.

What does each stage do?

  • Build runs pip install -r requirements.txt to install pytest inside the container. This mirrors what you would do locally before running tests.
  • Test runs python -m pytest tests/ --verbose --junit-xml=results.xml. The --junit-xml flag outputs results in a format Jenkins can parse and display.
  • Package runs tar -czf calculator-app.tar.gz app/ to bundle your application into a single distributable archive. Then archiveArtifacts stores that archive in Jenkins so anyone can download it from the build page.
Add a post section for test result reporting

The post section runs after all stages finish, regardless of whether the pipeline passed or failed. This is where you handle cleanup and reporting.

What does the post section do?

  • post { always { ... } } means this block runs every time, whether stages succeeded or failed.
  • junit 'results.xml' publishes the test results file to Jenkins. You will see a test results summary on every build page showing which tests passed and which failed.
  • Commit the updated Jenkinsfile to your Git repository by running these commands:
git add Jenkinsfile
git commit -m "Add Declarative Pipeline with Build, Test, and Package stages"

Why commit the Jenkinsfile?

Jenkins reads the pipeline definition from your Git repository. Committing ensures Jenkins can find the Jenkinsfile when it checks out your code. This is the core idea behind Pipeline-as-Code. Your build instructions live with your source code and evolve together.

  • Confirm the commit succeeded by running:
git log --oneline -1
  • You should see your latest commit message: Add Declarative Pipeline with Build, Test, and Package stages.

Commit failed?

  • If you see nothing to commit, make sure you saved the Jenkinsfile before running git add.
  • If Git asks you to set your name and email, run git config user.name "Your Name" and git config user.email "you@example.com" first, then retry the commit.
  • help me fix my git commit for the Jenkinsfile

✔️ Awesome, I've got everything!

Great. Double check you have saved Jenkinsfile and committed it to Git.

ⓧ I'd like to double check the full code

Here is the complete Jenkinsfile as it should look:

pipeline {
    agent {
        docker {
            image 'python:3.14.6-alpine3.24'
        }
    }

    stages {
        stage('Build') {
            steps {
                sh 'pip install -r requirements.txt'
            }
        }

        stage('Test') {
            steps {
                sh 'python -m pytest tests/ --verbose --junit-xml=results.xml'
            }
        }

        stage('Package') {
            steps {
                sh 'tar -czf calculator-app.tar.gz app/'
                archiveArtifacts artifacts: 'calculator-app.tar.gz', fingerprint: true
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}

Your pipeline definition is locked in and committed. Next up, you will create a Jenkins job that runs this pipeline and watch all three stages execute in real time.

Run the Pipeline in Jenkins

You have a complete Jenkinsfile that defines your build, test, and package stages. Now it's time to put it to work.

In this step, you'll create a Pipeline job inside Jenkins, paste your pipeline definition, and trigger your first automated build. You'll watch all three stages execute in real time and confirm that your tests pass.

In this step, get ready to:
  • Create a Pipeline job in the Jenkins dashboard.
  • Configure the job with your Declarative Pipeline definition.
  • Trigger the first build and verify all stages pass.
Create a new Pipeline job

Jenkins organizes work into jobs. A Pipeline job tells Jenkins to look for a Declarative Pipeline definition and execute it.

  • Return to Jenkins at http://localhost:8080 in your browser.
  • Click New Item in the left sidebar.
  • In the Enter an item name field, type python-calculator.
  • Scroll down and select Pipeline.
  • Click OK at the bottom of the page.

Jenkins opens the Pipeline configuration page for your new job.

Why the name matters

Jenkins uses the item name to create directories on disk. Avoid spaces in the name to prevent issues with scripts that handle file paths.

Configure the job with your pipeline script

You can point Jenkins at a Git repository to find your Jenkinsfile, or paste the pipeline definition directly. Pasting directly is the simplest approach for a local setup.

  • Scroll down to the Pipeline section at the bottom of the configuration page.
  • Confirm the Definition dropdown is set to Pipeline script.
  • Delete any placeholder text in the Script text area.
  • Paste the following pipeline definition into the Script text area:
pipeline {
    agent {
        docker {
            image 'python:3.14.6-alpine3.24'
        }
    }

    stages {
        stage('Build') {
            steps {
                sh 'pip install -r requirements.txt'
            }
        }

        stage('Test') {
            steps {
                sh 'python -m pytest tests/ --verbose --junit-xml=results.xml'
            }
        }

        stage('Package') {
            steps {
                sh 'tar -czf calculator-app.tar.gz app/'
                archiveArtifacts artifacts: 'calculator-app.tar.gz', fingerprint: true
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}

What does this pipeline do?

This is the same Jenkinsfile you wrote in the previous step. It tells Jenkins to spin up a fresh python:3.14.6-alpine3.24 Docker container and run three stages inside it.

  • Build installs your Python dependencies.
  • Test runs pytest and produces a JUnit XML report.
  • Package bundles your app into a .tar.gz archive and stores it as a build artifact.
  • The post section publishes test results to Jenkins regardless of whether the build passes or fails.
  • Click Save at the bottom of the configuration page.

Jenkins saves your job and takes you to the project dashboard for python-calculator.

Why paste instead of using SCM?

Pointing Jenkins at your local Git repository requires configuring mounted volume paths inside the Docker container. Pasting the script directly keeps things simple for this project.

In a real team environment, you would use "Pipeline script from SCM" so Jenkins pulls the Jenkinsfile from your repository automatically on every commit.

Trigger the build and verify the results

With the job configured, you can trigger your first pipeline run manually.

  • Click Build Now in the left sidebar of the python-calculator project page.

What happens when you click Build Now?

Jenkins queues a new build. It pulls the python:3.14.6-alpine3.24 Docker image (this may take a minute the first time), spins up a container, and executes each stage in order.

You can watch the progress in the Stage View that appears on the project page. Each stage shows a colored box that turns green when it completes successfully.

  • Wait for the build to finish. You should see three green boxes in the Stage View labeled Build, Test, and Package.
  • Click on build #1 in the Build History panel on the left sidebar.
  • Click Console Output in the left sidebar to see the full log of what Jenkins executed.

In the console output, you should see pytest running your five test functions and all of them passing.

  • Click Back to Project or navigate back to the python-calculator project page.
  • Click on build #1 again, then click Test Result in the left sidebar.

You should see your test results summary showing all 5 tests passing with 0 failures.

Check the archived artifact

On the build page for #1, you should also see a Build Artifacts section listing calculator-app.tar.gz. This confirms the Package stage created and stored your distributable archive.

Build failed or stuck?

If the build fails at the first stage, Jenkins may not be able to pull the Docker image. Make sure Docker Desktop is running and that the Docker-in-Docker container (jenkins-docker) is still active.

If the build hangs for a long time, the python:3.14.6-alpine3.24 image may still be downloading. Check the console output for download progress. The first run is always slower.

If you see a syntax error, double-check that you pasted the entire pipeline definition without accidentally cutting off the closing braces.

Help me debug my Jenkins pipeline build failure.

✔️ Awesome, I've got everything!

Great. You should have all three stages showing green checkmarks in the Stage View and your test results visible in the build summary.

ⓧ I'd like to double check the full code

Here is the complete pipeline definition that should be pasted into your Jenkins job:

pipeline {
    agent {
        docker {
            image 'python:3.14.6-alpine3.24'
        }
    }

    stages {
        stage('Build') {
            steps {
                sh 'pip install -r requirements.txt'
            }
        }

        stage('Test') {
            steps {
                sh 'python -m pytest tests/ --verbose --junit-xml=results.xml'
            }
        }

        stage('Package') {
            steps {
                sh 'tar -czf calculator-app.tar.gz app/'
                archiveArtifacts artifacts: 'calculator-app.tar.gz', fingerprint: true
            }
        }
    }

    post {
        always {
            junit 'results.xml'
        }
    }
}

Secret mission

Add a Code Quality Stage

Your pipeline builds, tests, and packages. But what about code quality? Real teams enforce coding standards automatically so that sloppy code never reaches production. In this secret mission, you will add a linting stage powered by flake8 that catches fatal errors and reports style warnings.

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 entirely in Docker containers on your local machine, so there are no ongoing costs.

Resources you used:

  • Docker containers: jenkins-docker (Docker-in-Docker) and jenkins-blueocean (Jenkins server).
  • Docker volumes: jenkins-data and jenkins-docker-certs.
  • Docker network: jenkins.
  • Docker image: myjenkins:2.555.3-1.

Keep everything running

No action needed. Choose this if you're still actively building or experimenting with your Jenkins pipeline.

  • Your Jenkins server stays accessible at http://localhost:8080.
  • The Docker containers will restart automatically if Docker Desktop restarts (thanks to the --restart=on-failure flag on the Jenkins container).
  • Both containers use local disk space but no external cloud resources.

Pause - I'll come back to this later

Stop the containers to free up memory and CPU. Your pipeline configuration, build history, and project files are all saved in the Docker volumes.

  • Stop both containers by running:
docker stop jenkins-blueocean jenkins-docker
  • When you're ready to resume, start them again by running:
docker start jenkins-docker jenkins-blueocean

Start order matters

Start jenkins-docker first because it provides the Docker-in-Docker environment that the Jenkins container depends on.

Delete - I don't want to use this again

Remove all Docker resources created during this project. This deletes your Jenkins configuration, build history, and all container data permanently.

  • Stop and remove the containers, volumes, network, and image by running these commands:
docker stop jenkins-blueocean jenkins-docker
docker rm jenkins-blueocean
docker volume rm jenkins-data jenkins-docker-certs
docker network rm jenkins
docker rmi myjenkins:2.555.3-1

Why only one container to remove?

The jenkins-docker container was started with the --rm flag, so Docker automatically removes it when it stops. Only jenkins-blueocean needs an explicit docker rm command.

  • Verify everything is cleaned up by running:
docker ps -a
docker volume ls
docker network ls

You should no longer see jenkins-blueocean, jenkins-docker, or the jenkins network in any of the outputs.

Nice Work!

Nice Work!

You just built a fully working CI/CD pipeline from scratch using Jenkins and Docker. Every time you hit "Build Now", your code is automatically installed, tested, and packaged without you lifting a finger.

You've learned how to:

  • Set up a Jenkins server running in Docker with Docker-in-Docker support for isolated pipeline execution.
  • Write a Declarative Pipeline (Jenkinsfile) that defines Build, Test, and Package stages using Docker agents.
  • Build a Python application with automated pytest-based tests and publish test results to the Jenkins UI.
  • Secret Mission: Added a code quality gate with flake8 that enforces code standards and fails the build when fatal errors are detected.

Ready to quiz yourself?