CI/CD Pipeline with Jenkins and Maven
Build a Jenkins pipeline that auto-builds and tests a Java app on every Git push.
Introduction
30 Second Summary
Every developer has experienced this pain: you write code that works on your machine, push it to the team, and something breaks. Maybe a test fails. Maybe the build is missing a dependency. The longer it takes to catch these issues, the harder they are to fix.
In this project, you will build a fully automated CI/CD pipeline using Jenkins, Maven, and Git. Your pipeline will automatically build, test, and package a Java application every time you push code.
What You'll Build
You will push code to GitHub, watch Jenkins detect the change within minutes, and see your app get built, tested, and packaged into a deployable artifact without you lifting a finger.
By the end of this project, you'll have:
- A live CI feedback loop where you push code to GitHub and watch Jenkins automatically detect the change, run your tests, and show you a pass or fail result within seconds.
- A 3-stage Declarative Pipeline (Build, Test, Deliver) that catches bugs by turning RED on a failing test, then turns GREEN after you fix it.
- A downloadable JAR artifact produced by every successful build, ready to deploy anywhere.
- Secret Mission: Containerize your app and deploy it to a local Kubernetes cluster using Minikube.
Are there any prerequisites?
You need Docker Desktop installed on macOS, Git installed, and a GitHub account. Jenkins runs inside a Docker container, so there is nothing else to install on your machine.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Set Up Jenkins and Maven
Every CI/CD pipeline needs a server that decides when to build, test, and deliver your code. Jenkins is that server. It watches your repository, kicks off builds, and reports results.
Jenkins also needs a build tool to compile Java projects. Maven handles that. In this step, you will get Jenkins running inside Docker and configure Maven so the pipeline can use it automatically.
In this step, get ready to:
- Run Jenkins in a Docker container and complete the setup wizard.
- Configure Maven as an auto-installed build tool.
- Confirm the Git plugin is ready for repository connections.
Run Jenkins in Docker and complete the setup wizard
Jenkins ships as a Docker image, so you do not need to install Java or Jenkins on your machine directly. One command spins up the entire server.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Start the Jenkins container by running this command:
docker run -d -p 8080:8080 -p 50000:50000 --restart=on-failure -v jenkins_home:/var/jenkins_home --name jenkins jenkins/jenkins:lts-jdk21
What does this command do?
- -d runs the container in the background so you keep your terminal.
- -p 8080:8080 maps port 8080 on your machine to Jenkins' web interface.
- -v jenkins_home:/var/jenkins_home creates a persistent volume so your data survives container restarts.
- --name jenkins gives the container a memorable name you can reference later.
- jenkins/jenkins:lts-jdk21 is the official Jenkins Long-Term Support image with Java 21 built in.
You should see a long container ID hash printed in your terminal. That confirms the container started successfully.
Don't see a container ID?
- If you see a message about port 8080 already in use, another app is using that port. Stop it or check for an existing container with docker ps.
- If you see "Cannot connect to the Docker daemon", make sure Docker Desktop is running.
Still stuck? Help me troubleshoot my Docker container not starting.
- Retrieve the initial admin password by running this command:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
A long alphanumeric string appears. Copy this password. You will paste it into the browser in a moment.
Seeing "Error: No such container"?
This means the container is not running yet. Wait 10-15 seconds for Jenkins to finish starting, then try the command again. You can check the container status with docker ps.
- Open your browser and navigate to http://localhost:8080.
- Paste the password into the Administrator password field.
- Click Continue.
- Click Install suggested plugins.
Jenkins shows a progress screen as it installs plugins. This takes 1-2 minutes.
What are suggested plugins?
Suggested plugins are a curated set of common plugins that cover most use cases, including Git integration, Pipeline support, and credentials management. You can add or remove plugins later from the Manage Jenkins > Plugins page.
- On the Create First Admin User page, fill in a username, password, full name, and email address.
- Click Save and Finish.
- On the Instance Configuration page, leave the default Jenkins URL and click Save and Finish.
- Click Start using Jenkins.
You land on the Jenkins dashboard. This is your CI/CD command center.
Configure Maven in Jenkins
Jenkins does not ship with Maven pre-installed. You need to tell Jenkins where to find Maven so your pipeline can compile and test Java code.
- In the left sidebar, click Manage Jenkins.
- Click Tools.
- Scroll down to the Maven installations section.
- Click Add Maven.
- In the Name field, type Maven.
- Make sure the Install automatically checkbox is checked.
- Click Save at the bottom of the page.
Why auto-install instead of a manual path?
When you check Install automatically, Jenkins downloads Maven on the first build that needs it. This is the same pattern used in real Jenkins environments. It means you never need to manually install Maven on the server.
The name Maven is what your Jenkinsfile will reference later in its tools block. The name must match exactly.
Jenkins returns you to the Manage Jenkins page. Maven is now configured and ready for your pipeline to use.
Verify Git plugin is installed
The Git plugin lets Jenkins clone repositories and detect changes. It should already be installed because you chose Install suggested plugins during the wizard. Let's confirm.
- In the left sidebar, click Manage Jenkins.
- Click Plugins.
- Click the Installed plugins tab.
- Type Git in the search filter at the top.
What do you expect to see in the filtered list?
You should see the Git plugin listed with a checkmark, confirming it was installed by the suggested plugins set.
Don't see the Git plugin?
- Switch to the Available plugins tab, search for Git, check the box next to it, and click Install.
- After installation, restart Jenkins by running docker restart jenkins in your terminal, then refresh your browser.
Still stuck? Help me find and install the Git plugin in Jenkins.
Your Jenkins server is running, Maven is configured, and Git integration is confirmed. Next up, you will create your first pipeline and connect it to a real GitHub repository.
Create Your First Pipeline
Your Jenkins server is running and knows how to find Maven. Now it needs something to build.
In this step you will connect Jenkins to a real Java project stored in Git and write your first Declarative Pipeline. By the end, you will have triggered your first successful build from the Jenkins dashboard.
In this step, get ready to:
- Fork and clone a sample Java app from GitHub.
- Write a Jenkinsfile with a Build stage.
- Create a Pipeline job in Jenkins and watch it go green.
Fork and clone the sample Java app
Jenkins needs source code to build. Instead of writing a Java app from scratch, you will fork an official sample project that already has tests and a Maven configuration ready to go.
- Open the sample repository in your browser by navigating to:
- https://github.com/jenkins-docs/simple-java-maven-app
- In the top-right corner of the page, click Fork.
- Under Owner, select your own GitHub account from the dropdown.
- Leave the repository name as simple-java-maven-app.
- Click Create fork.
GitHub will redirect you to your new fork. Now clone it to your local machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Clone your fork by running this command:
git clone https://github.com/[[GITHUB_USERNAME="your-github-username"]]/simple-java-maven-app.git
- Move into the project folder by running this command:
cd simple-java-maven-app
- Confirm the project files exist by running:
ls
You should see files like pom.xml, a src folder, and a jenkins folder in the output.
Clone failed or directory is empty?
Make sure you forked the repository first. Cloning the original without write access will block you from pushing later.
Double-check that you replaced the username in the clone URL with your actual GitHub username.
Help me troubleshoot my clone command.
Create a Jenkinsfile with a Build stage
A Jenkinsfile is a text file at the root of your repository that tells Jenkins what stages to run. You will start with a single Build stage that compiles the Java project using Maven.
- Create a new file named Jenkinsfile in the root of the simple-java-maven-app folder and paste the following code:
pipeline {
agent any
tools {
// Use the Maven tool configured in Jenkins
maven 'Maven'
}
stages {
stage('Build') {
steps {
// Compile and package, skipping tests for now
sh 'mvn -B -DskipTests clean package'
}
}
}
}
What does this code do?
- pipeline { agent any } tells Jenkins to run this pipeline on any available agent (your Jenkins server itself in this case).
- tools { maven 'Maven' } instructs Jenkins to auto-install and use the Maven tool you configured in Step 1.
- sh 'mvn -B -DskipTests clean package' runs Maven in batch mode, skipping tests, to compile the code and package it into a JAR file.
- Save the file.
- Commit and push the Jenkinsfile to your fork by running these commands:
git add Jenkinsfile && git commit -m "Add Jenkinsfile with Build stage" && git push
You should see output confirming the push to your fork on GitHub.
Push rejected or authentication error?
GitHub requires a personal access token for HTTPS pushes. If prompted, enter your GitHub username and a personal access token (not your password).
If you see "remote: Permission denied", confirm you cloned your own fork and not the original jenkins-docs repository.
Help me fix my GitHub push error.
✔️ Awesome, I've got everything!
Great. Double check you have saved and pushed your Jenkinsfile.
ⓧ I'd like to double check the full code
Your Jenkinsfile should look exactly like this:
pipeline {
agent any
tools {
maven 'Maven'
}
stages {
stage('Build') {
steps {
sh 'mvn -B -DskipTests clean package'
}
}
}
}
Create the Pipeline job in Jenkins
Your code is on GitHub. Now you need to tell Jenkins where to find it and how to run it.
- Switch back to the Jenkins dashboard at http://localhost:8080.
- Click New Item in the left sidebar.
- In the Enter an item name field, type simple-java-pipeline.
- Select Pipeline from the list of project types.
- Click OK.
You are now on the Pipeline configuration page.
- Scroll down to the Pipeline section at the bottom.
- In the Definition dropdown, select Pipeline script from SCM.
- In the SCM dropdown, select Git.
- In the Repository URL field, enter your fork's HTTPS URL. It looks like:
https://github.com/[[GITHUB_USERNAME="your-github-username"]]/simple-java-maven-app.git
- Under Branches to build, confirm the Branch Specifier is set to */master.
- Click Save.
What is Pipeline script from SCM?
This option tells Jenkins to look for a file called Jenkinsfile in your Git repository instead of you pasting the pipeline code into the Jenkins UI. Keeping your pipeline definition in source control means it evolves alongside your application code.
Before you trigger the build, what do you think will happen? Will it succeed or fail given that you just pushed a valid Jenkinsfile with a Build stage?
- Click Build Now in the left sidebar.
Jenkins clones your repository, finds the Jenkinsfile, and runs the Build stage. After a minute or so, you should see a green checkmark next to build #1.
- Click the build number (#1) to open the build details.
- Click Console Output in the left sidebar.
- Scroll to the bottom and confirm you see BUILD SUCCESS in the Maven output followed by Finished: SUCCESS.
Build failed or stuck in queue?
If the build fails with a Git error, double-check that the Repository URL matches your fork exactly and uses HTTPS.
If you see a Maven error, confirm you named the Maven tool exactly Maven in Manage Jenkins > Tools. The name in your Jenkinsfile must match the configured tool name.
Help me diagnose my failed Jenkins build.
Your first pipeline just went green. Next up, you will add automated testing and learn what happens when tests fail.
Add Testing and Break the Build
In the last step, you connected Jenkins to your GitHub repository and ran your first successful build. That build compiled your Java code, but it deliberately skipped the tests. A pipeline that only builds is not truly doing Continuous Integration.
CI means every push is automatically built AND tested. In this step, you will add automated tests to your pipeline and experience the CI feedback loop firsthand. You will intentionally break a test, watch the pipeline catch it, diagnose the failure, fix it, and watch it recover.
In this step, get ready to:
- Add a Test stage to your Jenkinsfile that runs your unit tests.
- Break the build by changing application code so a test fails.
- Fix the failure and restore a green pipeline.
Add a Test stage to your Jenkinsfile
Your current Jenkinsfile has a Build stage that runs Maven with the -DskipTests flag. Now you will add a dedicated Test stage that actually runs the project's unit tests.
- Open Jenkinsfile in your code editor (it is in the root of your simple-java-maven-app folder).
- Find the closing } of the Build stage, and add the following Test stage directly below it (still inside the stages block):
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
What does this code do?
This adds a second pipeline stage called Test that runs after Build completes.
- sh 'mvn test' tells Maven to compile and run every unit test in the project.
- The post { always { junit ... } } block publishes JUnit test results to Jenkins regardless of pass or fail. This gives you a visual test report inside the Jenkins UI.
- Save Jenkinsfile.
- Commit and push the updated Jenkinsfile by running these commands in your terminal:
git add Jenkinsfile && git commit -m "Add Test stage" && git push
- Switch back to Jenkins at http://localhost:8080.
- Click on your simple-java-pipeline job, then click Build Now in the left sidebar.
- Wait for the build to finish. You should see two green stages in the Stage View: Build and Test.
Build failed on the Test stage?
- Click the red build number, then click Console Output. Look for syntax errors in your Jenkinsfile (missing braces, incorrect indentation).
- Make sure the Test stage is inside the stages { } block, not outside it.
- Confirm the post block is inside the stage('Test') block, not at the pipeline level.
Help me fix my Jenkinsfile Test stage syntax
✔️ Awesome, I've got everything!
Great. Double check you have saved your Jenkinsfile and your Jenkins build shows two green stages.
ⓧ I'd like to double check the full code
Here is the full Jenkinsfile as it should look right now:
pipeline {
agent any
tools {
maven 'Maven'
}
stages {
stage('Build') {
steps {
sh 'mvn -B -DskipTests clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
}
}
}
}
}
Break the build intentionally
Your pipeline now builds and tests on every push. Time to see what happens when a test fails.
- In your code editor, open src/main/java/com/mycompany/app/App.java.
- Find this line:
private static final String MESSAGE = "Hello World!";
- Change "Hello World!" to "Hello CI!", so the line now looks like this:
private static final String MESSAGE = "Hello CI!";
- Save App.java.
- Commit and push the change by running:
git add -A && git commit -m "Change greeting message" && git push
Before you trigger the build, think about what you expect to happen. The unit test in AppTest.java asserts that getMessage() returns "Hello World!". Will the Test stage pass or fail?
- In Jenkins, click Build Now on your simple-java-pipeline job.
- Watch the Stage View. The Build stage goes green, but the Test stage goes RED.
This is exactly what should happen. Your pipeline caught the mismatch between your code and the test's expected value. This is CI doing its job.
- Click the red build number (e.g. #3) in the Build History.
- Click Test Result in the left sidebar.
- You will see which test failed and the assertion message showing it expected "Hello World!" but got "Hello CI!".
What does the test report tell you?
The Test Result page shows every test that ran, which ones passed, and which ones failed. For the failed test, Jenkins displays the expected value vs. the actual value.
In a real team, this report tells you exactly what broke and where to look, saving you from hunting through logs manually.
Fix the failing test
You have diagnosed the problem. Now revert the change to restore the original greeting and complete the CI feedback loop.
- In App.java, find this line:
private static final String MESSAGE = "Hello CI!";
- Change "Hello CI!" back to "Hello World!", so it looks like this:
private static final String MESSAGE = "Hello World!";
- Save App.java.
- Commit and push the fix by running:
git add -A && git commit -m "Fix greeting to pass test" && git push
Before you trigger the build, predict the result. You have restored the original string that the test expects. What color do you expect the Test stage to show this time?
- In Jenkins, click Build Now again.
- Watch the Stage View. Both stages go green. Click the build number, then click Test Result to see all tests passing.
This is the CI feedback loop
You just experienced the core cycle that CI automates for development teams: push code, tests catch a bug, diagnose the failure from the report, fix the code, push again, and confirm the pipeline is green.
Without this loop, bugs sneak into production unnoticed. With it, you catch problems within seconds of introducing them.
Your pipeline now catches bugs automatically. Next up, you will add a Deliver stage that packages your app into a deployable JAR and set up automatic triggers so Jenkins builds on every push without you clicking Build Now.
Deliver Artifacts and Automate Triggers
In the last step, you experienced the CI feedback loop firsthand. You broke a build, diagnosed the failure through test reports, and fixed it.
Your pipeline catches bugs, but it still produces nothing you could hand to someone and say "deploy this." It also requires you to click Build Now every time. In this step, you will add a Deliver stage that packages a deployable JAR artifact, then configure automatic triggers so Jenkins builds on every push without manual intervention.
In this step, get ready to:
- Add a Deliver stage that runs the deliver script and archives the JAR.
- Configure SCM polling so Jenkins automatically detects new pushes.
- Test the full automated cycle from push to artifact.
Add a Deliver stage to the Jenkinsfile
The sample repository includes a deliver script at jenkins/scripts/deliver.sh that runs the packaged JAR and prints its output. You will add a pipeline stage that calls this script and then archives the resulting JAR so it is downloadable from Jenkins.
- In your terminal, make sure you are inside the simple-java-maven-app folder.
- Open the Jenkinsfile in your editor.
- Find the closing } of the tools block. Add the following options block directly below it, before stages {:
options {
skipStagesAfterUnstable()
}
- Now scroll down to the end of the Test stage. Find the closing } of stage('Test'). Add the following Deliver stage directly below it, still inside the stages block:
stage('Deliver') {
steps {
sh './jenkins/scripts/deliver.sh'
archiveArtifacts artifacts: 'target/*.jar'
}
}
What does this code do?
These two additions work together to finalize your pipeline:
- options { skipStagesAfterUnstable() } tells Jenkins to skip remaining stages if any earlier stage marks the build as unstable. This prevents delivering a JAR from code that has test warnings.
- sh './jenkins/scripts/deliver.sh' runs the sample app's deliver script, which executes the packaged JAR and prints output to prove it works.
- archiveArtifacts artifacts: 'target/*.jar' saves the built JAR file to Jenkins so you can download it from the build page.
- Save Jenkinsfile.
- Commit and push your changes by running this command:
git add Jenkinsfile && git commit -m "Add Deliver stage and options" && git push
- Switch back to Jenkins at http://localhost:8080.
- Click on simple-java-pipeline to open your job.
- Click Build Now in the left sidebar.
Wait for the build to finish. You should see three green stages: Build, Test, and Deliver.
- Click the build number (e.g. #3) in the Build History panel to open the build page.
- Look for the Build Artifacts section on the build page. You should see a .jar file listed there.
Don't see Build Artifacts?
- Check the console output (click the build number, then Console Output) for errors in the Deliver stage. A common issue is a missing executable permission on the deliver script. Jenkins fetches the file from Git, which should preserve permissions.
- Make sure the archiveArtifacts line is inside the steps { } block of the Deliver stage, not outside it.
- Confirm the Build stage passed first. If mvn -B -DskipTests clean package failed, there is no JAR to archive.
Still stuck? Why isn't my Deliver stage archiving the JAR?
✔️ Awesome, I've got everything!
Great. Double check you have saved your Jenkinsfile and your build shows all three stages green with the JAR artifact available.
ⓧ I'd like to double check the full code
Your Jenkinsfile should contain these sections in order:
- pipeline { wrapper with agent any.
- tools { maven 'Maven' } block.
- options { skipStagesAfterUnstable() } block (new in this step).
- stages { containing three stages: Build, Test (with post { always { junit ... } }), and Deliver (new in this step).
- The Deliver stage runs sh './jenkins/scripts/deliver.sh' and archiveArtifacts artifacts: 'target/*.jar'.
Scroll up and confirm each section matches what you pasted earlier.
Configure SCM polling for automatic triggers
Right now you still have to click Build Now manually. SCM polling tells Jenkins to check your repository on a schedule and automatically start a build when it detects new commits.
- In Jenkins, click simple-java-pipeline to open your job.
- Click Configure in the left sidebar.
- Scroll down to the Build Triggers section.
- Check the Poll SCM checkbox.
- In the Schedule field that appears, enter the following:
H/2 * * * *
- Click Save at the bottom of the page.
What does H/2 * * * * mean?
This is Jenkins cron syntax. The five fields represent minute, hour, day-of-month, month, and day-of-week. H/2 means "every 2 minutes, offset by a hash of the job name." The H spreads polling evenly across jobs so they do not all hit the server at the same second.
In practice, Jenkins checks your GitHub repository roughly every 2 minutes. If it finds a new commit since the last build, it triggers a build automatically.
Test the full automated cycle
With polling active, any push to your fork triggers a build within 2 minutes. Time to prove it works end to end.
- Switch back to your terminal in the simple-java-maven-app folder.
- Make a small change to the README and push it by running these commands:
echo "\n# Automated CI/CD" >> README.md
git add README.md
git commit -m "Trigger automated build"
git push
Before you check Jenkins, predict: do you think Jenkins will start a build on its own within the next 2 minutes, or will you need to click Build Now?
- Switch back to Jenkins and open simple-java-pipeline.
- Wait up to 2 minutes. Watch the Build History panel on the left.
A new build appears automatically. You did not click anything. Jenkins detected the new commit via polling and started the pipeline on its own.
- Once the build finishes, click the build number to open the build page.
- Confirm all three stages (Build, Test, Deliver) show green.
- Confirm the Build Artifacts section lists the .jar file.
- Click the JAR file name to download it. This is the deployable artifact your pipeline produced.
Build didn't start automatically?
- Confirm you saved the Poll SCM configuration. Return to Configure and check that Poll SCM is still checked with H/2 * * * * in the schedule.
- Make sure your push actually went through. Run git log --oneline -1 and then check your GitHub fork to see the commit appeared.
- The first poll cycle may take up to 2 minutes from when you saved. Give it the full window before troubleshooting further.
Still stuck? Why isn't Jenkins automatically detecting my push?
Why polling instead of webhooks?
In production, teams use webhooks so GitHub notifies Jenkins the instant a push happens. Webhooks require exposing Jenkins to the internet, which adds complexity unrelated to learning pipelines.
Polling achieves the same automatic behavior with zero extra infrastructure. The 2-minute delay is a small trade-off for simplicity.
Secret mission
Deploy to Kubernetes with Minikube
Your pipeline builds, tests, and packages your app into a JAR. But where does that JAR actually run? In this extension, you'll deploy your application to a local Kubernetes cluster using Minikube, completing the Continuous Delivery half of CI/CD.
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 locally via Docker, so there are no ongoing costs.
Resources you used:
- Jenkins Docker container (jenkins).
- Docker volume (jenkins_home) storing all Jenkins data.
- Local project folder (simple-java-maven-app) on your Desktop.
- GitHub fork of simple-java-maven-app.
- Minikube cluster (Secret Mission only).
Keep everything running
No action needed. Choose this if you are still actively building or want to experiment with your Jenkins pipeline further.
- Your Jenkins container is set to restart automatically with Docker Desktop thanks to the --restart=on-failure flag.
- Your Minikube cluster remains running in the background (if you completed the Secret Mission).
- Your GitHub fork stays in your account for future reference.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping all your data intact.
- Stop the Jenkins container by running:
- Resume later with docker start jenkins. All your pipeline data persists in the jenkins_home volume.
docker stop jenkins
- If you completed the Secret Mission, stop Minikube by running:
minikube stop
Your cluster state is preserved. Run minikube start to resume it later.
Delete - I don't want to use this again
Remove all project resources and start fresh. This cannot be undone.
- Remove the Jenkins container and its data volume by running:
docker rm -f jenkins
docker volume rm jenkins_home
- Delete the local project folder by running:
rm -rf ~/Desktop/simple-java-maven-app
- Delete your GitHub fork by opening your fork's page on GitHub.
- Click the Settings tab at the top of the repository.
- Scroll down to the Danger Zone section.
- Click Delete this repository and follow the prompts to confirm.
- If you completed the Secret Mission, delete your Minikube cluster by running:
minikube delete
Nice Work!
Nice Work!
You just built a fully automated CI/CD pipeline using Jenkins, Maven, and Git. From a blank Docker container to a 3-stage pipeline that builds, tests, and delivers your Java app on every push.
Here's what you accomplished:
- Set up Jenkins from scratch using Docker and created a Declarative Pipeline connected to a Git repository with Maven auto-installation.
- Experienced the CI feedback loop firsthand by intentionally breaking a build with a failing test, diagnosing the failure through JUnit test reports, and pushing a fix to turn the pipeline green.
- Added artifact archiving and SCM polling to produce a deployable JAR and trigger builds automatically on every Git push.
- Secret Mission: Containerized your app with a Dockerfile and deployed it to a local Kubernetes cluster using Minikube.
Ready to quiz yourself?