Build a CI/CD Pipeline with Jenkins

Automate build, test, analysis, and containerization with Jenkins.

Introduction

30 Second Summary

Every time a developer pushes code, someone has to check if it compiles, if the tests pass, if the code quality is acceptable, and if it can be packaged for deployment. Doing all of that manually is slow, error-prone, and honestly a bit boring.

In this project, you will build an automated CI/CD pipeline using Jenkins that compiles a Java application, runs tests, analyzes code quality, builds a Docker image, and sends notifications to Slack.

What You'll Build

A complete CI/CD pipeline that automatically builds, tests, analyzes, and containerizes a Java application every time code is pushed to GitHub.

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

  • A Jenkinsfile (Declarative Pipeline) with 5 automated stages: Checkout, Build, Test, SonarQube Analysis, and Docker Build.
  • A running SonarQube instance integrated with Jenkins to enforce code quality on every build.
  • Real-time Slack notifications that report build success or failure to your team channel.
  • Secret Mission: Add a GitHub webhook so the pipeline triggers automatically on every push, simulating a real-world CI/CD workflow.

Are there any prerequisites?

This project assumes you have a Linux Ubuntu system with Docker and Jenkins already installed and running. You also need a GitHub account. Basic familiarity with Linux commands and Docker is helpful.

Before We Start

Before We Start...

Install Prerequisites and Prepare the System

Jenkins is installed and running, but it can't build a Java project on its own. It needs tools like Git, Maven, and the Java JDK installed on your system.

In this step, you'll install those tools, grant Jenkins the right permissions to use Docker, and tune your Linux kernel for SonarQube.

In this step, get ready to:
  • Install Git, Java 21 JDK, and Maven.
  • Grant Jenkins permission to use Docker and fix workspace ownership.
  • Configure Linux kernel parameters for SonarQube.
Install Git, Java 21 JDK, and Maven

Jenkins needs Git to clone repositories from GitHub, the Java 21 JDK to compile code, and Maven to build and test your application. All three must be available on the system PATH so Jenkins can find them during pipeline runs.

  • Update your package list and install all three tools by running these commands:
sudo apt update
sudo apt install -y git openjdk-21-jdk maven

What did I just install?

  • git lets Jenkins clone your code repositories from GitHub.
  • openjdk-21-jdk provides the Java 21 compiler and runtime that Maven uses to build your project.
  • maven is the build tool that compiles Java code, runs tests, and packages the application into a JAR file.
  • Verify all three tools installed correctly by running these commands:
git --version
java -version
mvn -version

You should see version numbers for all three tools. The exact versions may vary depending on your Ubuntu release.

Seeing 'Unable to locate package'?

  • Make sure you ran sudo apt update first. Without it, your system doesn't know about the latest available packages.
  • The openjdk-21-jdk package requires Ubuntu 22.04 or later. Run lsb_release -a to check your Ubuntu version.
  • Help me troubleshoot package installation errors on Ubuntu.
Grant Jenkins permission to use Docker and fix workspace ownership

The Jenkins service runs as a system user called jenkins. By default, this user cannot access the Docker socket and may not own its workspace directory correctly. Both issues cause pipeline failures that are hard to diagnose later.

  • Add the jenkins user to the docker group and fix workspace ownership by running these commands:
sudo usermod -aG docker jenkins
sudo chown -R jenkins:jenkins /var/lib/jenkins

Why does Jenkins need these permissions?

Without docker group membership, every Docker command in your pipeline fails with a "permission denied" error at /var/run/docker.sock.

The chown command ensures Jenkins owns its workspace directory. Incorrect ownership is a common Ubuntu issue that causes "Could not init" and "Permission denied" errors during Git clone.

  • Configure Git to trust the Jenkins workspace directory by running:
sudo -u jenkins git config --global --add safe.directory '*'

What is 'safe.directory'?

Git has a security check that blocks operations in directories owned by a different user. Since Jenkins clones code into a system-owned workspace, Git's "dubious ownership" check would block every clone. This command tells Git to trust all directories for the jenkins user.

  • Restart Jenkins to apply the group membership change by running:
sudo systemctl restart jenkins
  • Wait about 30 seconds for Jenkins to restart, then verify it is running by running:
sudo systemctl status jenkins

You should see active (running) in green text in the output.

  • Confirm the workspace ownership is correct by running:
ls -ld /var/lib/jenkins/workspace

You should see jenkins jenkins in the ownership columns of the output.

Jenkins not starting back up?

  • Check the Jenkins logs for errors by running sudo journalctl -u jenkins -n 50. Look for port conflicts or Java errors.
  • Make sure no other process is using port 8080. Run sudo lsof -i :8080 to check.
  • Help me troubleshoot Jenkins not starting after restart.
Configure Linux kernel parameters for SonarQube

SonarQube uses embedded Elasticsearch to index your code for analysis. Elasticsearch requires specific Linux kernel settings to run properly. Without them, the SonarQube container crashes silently on startup.

  • Create a kernel configuration file for SonarQube by running this command:
sudo tee /etc/sysctl.d/99-sonarqube.conf <<EOF
vm.max_map_count=262144
fs.file-max=131072
EOF

What are these kernel settings?

  • vm.max_map_count=262144 increases the maximum number of memory map areas a process can have. Elasticsearch requires at least this many. Ubuntu's default of 65530 is far too low.
  • fs.file-max=131072 raises the system-wide limit on open file descriptors. Some Ubuntu systems have lower defaults that cause Elasticsearch to fail.

The file at /etc/sysctl.d/99-sonarqube.conf persists these settings so they survive system reboots.

  • Apply the new kernel parameters by running:
sudo sysctl --system
  • Verify the parameters are set by running:
sysctl vm.max_map_count fs.file-max

You should see vm.max_map_count = 262144 and fs.file-max = 131072 in the output.

Values not showing the expected numbers?

  • Make sure the tee command ran successfully. Check the file contents with cat /etc/sysctl.d/99-sonarqube.conf. You should see exactly two lines with the settings.
  • If the file looks correct, try running sudo sysctl --system again. The output should list your config file as it applies each setting.
  • Help me troubleshoot sysctl kernel parameter settings on Ubuntu.

✔️ Awesome, I've got everything!

Make sure you've saved the config file. You can move on to the next substep.

ⓧ I'd like to double check the full code

Here is what /etc/sysctl.d/99-sonarqube.conf should contain:

vm.max_map_count=262144
fs.file-max=131072

Your system is fully prepped. Next up, you'll start SonarQube via Docker and install the Jenkins plugins that connect everything together.

Start SonarQube and Install Jenkins Plugins

Your system now has all the foundational tools installed. Git, Java, and Maven are ready, and Jenkins has permission to run Docker containers.

The next piece of the puzzle is code quality analysis. In this step, you'll start a SonarQube instance using Docker and wire it into Jenkins so your pipeline can automatically scan code for bugs and vulnerabilities.

In this step, get ready to:
  • Start a SonarQube container and verify it's running.
  • Generate a SonarQube authentication token for Jenkins.
  • Install the required Jenkins plugins and configure the SonarQube connection.
Start SonarQube via Docker

SonarQube is an open-source platform that performs static code analysis. It scans your source code for bugs, security vulnerabilities, and code smells before your application ever runs.

  • Start the SonarQube Community Build container by running this command:
docker run -d --name sonarqube \
  -p 9000:9000 \
  -v sonarqube_data:/opt/sonarqube/data \
  -v sonarqube_logs:/opt/sonarqube/logs \
  -v sonarqube_extensions:/opt/sonarqube/extensions \
  sonarqube:community

What does this command do?

  • sonarqube:community is the free edition of SonarQube, bundled as a Docker image with everything it needs to run.
  • -d runs the container in the background so you can keep using your terminal.
  • -p 9000:9000 maps port 9000 on your machine to port 9000 inside the container, so you can access SonarQube in your browser.
  • The three -v flags create named Docker volumes for data, logs, and extensions. These persist your SonarQube data even if the container is removed.

SonarQube takes 2-3 minutes to fully start. Its embedded search engine needs time to initialize before the web interface becomes available.

  • Wait about 2-3 minutes, then open your browser and go to http://localhost:9000.
  • You should see the SonarQube login page.

Monitor the startup progress

If you want to watch SonarQube boot up in real time, run docker logs -f sonarqube in a separate terminal. When you see "SonarQube is operational", it's ready. Press Ctrl+C to stop following the logs.

SonarQube page not loading?

  • Give it more time. SonarQube can take up to 3 minutes on slower machines. Refresh the page after waiting.
  • Check that the container is actually running by running docker ps. You should see sonarqube in the list.
  • If the container exited, the kernel parameters from Step 1 may not be active. Run sysctl vm.max_map_count and confirm it shows 262144. If not, re-run sudo sysctl -w vm.max_map_count=262144 and start a fresh container.
  • If port 9000 is already in use, you'll see a bind error. Stop whatever is using that port or map SonarQube to a different port.

Still stuck? Help me troubleshoot why my SonarQube Docker container isn't starting.

Generate a SonarQube authentication token

Jenkins needs a way to authenticate with SonarQube when it sends code for analysis. Instead of using a username and password, you'll create a dedicated token that Jenkins can use.

  • On the SonarQube login page, enter admin for both the username and password.
  • Click Log in.
  • SonarQube will prompt you to change the default password. Enter a new password you'll remember and click Update.

You're now logged into the SonarQube dashboard. Time to create the authentication token.

  • Click your profile icon in the top-right corner of the page.
  • Click My Account.
  • Click the Security tab.
  • In the Generate Tokens section, type jenkins in the Name field.
  • Select Global Analysis Token from the Type dropdown.
  • Leave the expiration set to No expiration.
  • Click Generate.
  • Copy the token value immediately and paste it somewhere safe (like a text editor). Once you leave this page, you will not be able to view the token again.

Why a Global Analysis Token?

SonarQube offers three token types: User, Project Analysis, and Global Analysis. A Project Analysis Token requires an existing SonarQube project, which you haven't created yet.

A Global Analysis Token lets Jenkins run analyses on any project. When Maven sends code to SonarQube for the first time in a later step, it will automatically create the project entry.

Install Jenkins plugins and configure SonarQube

Jenkins needs three plugins to power your pipeline. The SonarQube Scanner plugin connects Jenkins to SonarQube. The Docker Pipeline plugin lets your pipeline build Docker images. The Slack Notification plugin sends build alerts to a Slack channel.

  • Open your Jenkins dashboard at http://localhost:8080.
  • Click Manage Jenkins in the left sidebar.
  • Click Plugins.
  • Click the Available plugins tab.
  • In the search box, type SonarQube Scanner and check the box next to it.
  • Clear the search box, type Docker Pipeline and check the box next to it.
  • Clear the search box, type Slack Notification and check the box next to it.
  • Click Install at the top of the list.
  • Wait for all three plugins and their dependencies to finish downloading.
  • Check the Restart Jenkins when installation is complete and no jobs are running checkbox at the bottom.
  • Wait for Jenkins to restart. Refresh the page until the login screen appears, then log back in.

Available plugins list is empty?

Jenkins may not have fetched plugin metadata yet. Go back to Manage Jenkins > Plugins > Advanced settings and click Check now to force Jenkins to contact the update center.

Need more help? Help me troubleshoot why the Jenkins plugin list is empty.

With the plugins installed, you now need to tell Jenkins where your SonarQube server lives and give it the authentication token you created earlier.

  • Click Manage Jenkins in the left sidebar.
  • Click System (under the System Configuration section).
  • Scroll down to the SonarQube servers section.
  • Check the Environment variables checkbox.
  • Click Add SonarQube.
  • Enter SonarQube in the Name field.
  • Enter http://localhost:9000 in the Server URL field.

Now you need to add your SonarQube token as a Jenkins credential. You can do this directly from this configuration page.

  • Next to the Server authentication token dropdown, click Add, then click Jenkins.
  • In the credentials dialog, set Kind to Secret text.
  • Paste the SonarQube token you copied earlier into the Secret field.
  • Enter sonarqube-token in the ID field.
  • Click Add to save the credential.
  • Back on the System configuration page, select the newly created sonarqube-token from the Server authentication token dropdown.
  • Click Save at the bottom of the page.

What did you just configure?

The SonarQube server entry you created tells Jenkins where to send code for analysis. The name SonarQube must match exactly what you'll reference later in the Jenkinsfile with withSonarQubeEnv('SonarQube').

The credential ID sonarqube-token is also referenced in the Jenkinsfile. Mismatched names are one of the most common pipeline failures.

  • Go back to Manage Jenkins > System and scroll to the SonarQube servers section to confirm your server entry shows the name SonarQube, the URL http://localhost:9000, and the sonarqube-token credential selected.

Don't see the SonarQube servers section?

The SonarQube Scanner plugin may not have installed correctly. Go to Manage Jenkins > Plugins > Installed plugins and search for SonarQube Scanner. If it's not there, reinstall it and restart Jenkins.

Need more help? Help me troubleshoot why the SonarQube servers section is missing in Jenkins.

SonarQube is running and Jenkins knows how to talk to it. Next up, you'll create the Java project that your pipeline will build, test, and analyze.

Create the Java/Maven Project and Push to GitHub

Your SonarQube server is running and Jenkins is loaded with the plugins it needs. Now it's time to create the actual application that your pipeline will build, test, and analyze.

In this step, you'll scaffold a Java project using Maven, write the application code and unit tests, then push everything to GitHub so Jenkins can pull it into the pipeline.

In this step, get ready to:
  • Scaffold a Java/Maven project using the Maven archetype generator.
  • Add SonarQube analysis support and a Dockerfile to the project.
  • Push the complete project to a GitHub repository.
Scaffold the Maven project

Maven provides project templates called archetypes that generate a standard folder structure, a starter class, and a test file. You'll use the maven-archetype-quickstart archetype to create a simple Java project in seconds.

  • Move to your home directory by running this command:
cd ~
  • Scaffold the Maven project by running this command:
mvn archetype:generate \
  -DgroupId=com.example \
  -DartifactId=cicd-pipeline-app \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DarchetypeVersion=1.4 \
  -DinteractiveMode=false

What does this command do?

This command tells Maven to generate a new project from a template. Here's what each flag does:

  • -DgroupId=com.example sets the package namespace for your Java code.
  • -DartifactId=cicd-pipeline-app names the project folder and the output JAR file.
  • -DinteractiveMode=false skips interactive prompts so the command runs with the values you provided.
  • Move into the new project directory by running:
cd cicd-pipeline-app
  • Verify the project structure by running:
ls -R

You should see a pom.xml file and a src/ directory containing main/java/com/example/App.java and test/java/com/example/AppTest.java.

Seeing 'mvn: command not found'?

Maven may not be installed or not on your PATH. Revisit Step 1 and confirm Maven is installed by running mvn -version.

Help me troubleshoot why Maven isn't found on my Ubuntu system

The archetype generated placeholder code in App.java and AppTest.java. You'll now replace both files with the actual application code that includes a greet() method, an add() method, and five JUnit tests.

  • Open App.java in a text editor by running:
nano src/main/java/com/example/App.java
  • Select all existing content by pressing Ctrl+A, then delete it.
  • Paste the following code into the file:
package com.example;

public class App {
    // Returns a personalized greeting, or a default if no name is given
    public String greet(String name) {
        if (name == null || name.trim().isEmpty()) {
            return "Hello, World!";
        }
        return "Hello, " + name + "!";
    }

    // Simple addition method for testing
    public int add(int a, int b) {
        return a + b;
    }

    // Entry point that demonstrates both methods
    public static void main(String[] args) {
        App app = new App();
        System.out.println(app.greet("Jenkins Pipeline"));
        System.out.println("2 + 3 = " + app.add(2, 3));
    }
}

What does this code do?

  • greet() returns a personalized greeting if a name is provided, or "Hello, World!" if the input is null or blank.
  • add() performs simple addition. Both methods give you something meaningful to test with JUnit.
  • main() is the entry point that runs when the JAR is executed directly.
  • Save the file by pressing Ctrl+O, then press Enter to confirm.
  • Exit nano by pressing Ctrl+X.

Nano save shortcut

You'll edit several files in this step. Each time, the workflow is the same: Ctrl+O to save, Enter to confirm the filename, and Ctrl+X to exit.

  • Now open AppTest.java by running:
nano src/test/java/com/example/AppTest.java
  • Delete all existing content.
  • Paste the following test code:
package com.example;

import static org.junit.Assert.*;
import org.junit.Test;

public class AppTest {

    // Verify greet() returns a personalized message
    @Test
    public void testGreetWithName() {
        App app = new App();
        assertEquals("Hello, Jenkins!", app.greet("Jenkins"));
    }

    // Verify greet() handles null input gracefully
    @Test
    public void testGreetWithNull() {
        App app = new App();
        assertEquals("Hello, World!", app.greet(null));
    }

    // Verify greet() handles blank/whitespace input
    @Test
    public void testGreetWithEmpty() {
        App app = new App();
        assertEquals("Hello, World!", app.greet("  "));
    }

    // Verify add() with positive numbers
    @Test
    public void testAdd() {
        App app = new App();
        assertEquals(5, app.add(2, 3));
    }

    // Verify add() with negative numbers
    @Test
    public void testAddNegative() {
        App app = new App();
        assertEquals(-1, app.add(2, -3));
    }
}

What do these tests cover?

These five tests exercise both methods with different inputs:

  • testGreetWithName checks that a name produces a personalized greeting.
  • testGreetWithNull and testGreetWithEmpty confirm the fallback to "Hello, World!" for bad input.
  • testAdd and testAddNegative verify addition with positive and negative numbers.

Jenkins will run these tests automatically in every pipeline build and report the results.

  • Save and exit nano (Ctrl+O, Enter, Ctrl+X).
  • Verify both the code and tests compile by running:
mvn test

You should see BUILD SUCCESS with Tests run: 5, Failures: 0, Errors: 0 in the output.

Tests failing or not compiling?

  • If you see a compilation error, make sure you deleted ALL the original placeholder code before pasting. Leftover lines from the archetype will cause conflicts.
  • If you see package com.example does not exist, check that the package com.example; line is the very first line in both files.
  • If you see a Java version error, confirm OpenJDK 21 is installed by running java -version.

Help me debug why mvn test is failing in my cicd-pipeline-app project

Add the SonarQube plugin and a Dockerfile

The archetype generated a basic pom.xml, but it's missing the plugins your pipeline needs. You'll replace it with a version that includes the SonarQube Maven plugin for static code analysis and the maven-jar-plugin for packaging your app into a runnable JAR.

  • Open pom.xml by running:
nano pom.xml
  • Delete all existing content.
  • Paste the following complete POM configuration:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>cicd-pipeline-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>cicd-pipeline-app</name>

    <!-- Compile with Java 21 -->
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- JUnit 4 for unit testing -->
    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- SonarQube scanner for static code analysis -->
            <plugin>
                <groupId>org.sonarsource.scanner.maven</groupId>
                <artifactId>sonar-maven-plugin</artifactId>
                <version>4.0.0.4121</version>
            </plugin>
            <!-- Package the app as a runnable JAR -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

What's in this POM?

  • The properties section sets the Java compiler to version 21, matching the JDK you installed in Step 1.
  • JUnit 4.13.2 is the test framework your pipeline will run.
  • The sonar-maven-plugin (4.0.0.4121) enables the mvn sonar:sonar command that sends code to SonarQube for analysis.
  • The maven-jar-plugin sets com.example.App as the main class, so the JAR is runnable with java -jar.
  • Save and exit nano.

Next, you'll create a Dockerfile that packages your compiled JAR into a lightweight container image. Your pipeline will use this in its final stage.

  • Create a new Dockerfile in the project root by running:
nano Dockerfile
  • Paste the following content:
# Use a minimal Java 21 runtime image
FROM eclipse-temurin:21-jre-alpine
# Set the working directory inside the container
WORKDIR /app
# Copy the compiled JAR from Maven's output
COPY target/cicd-pipeline-app-1.0-SNAPSHOT.jar app.jar
# Document the port the app listens on
EXPOSE 8080
# Run the JAR when the container starts
ENTRYPOINT ["java", "-jar", "app.jar"]

What does this Dockerfile do?

  • FROM eclipse-temurin:21-jre-alpine starts from a minimal Alpine Linux image with just the Java 21 runtime. This keeps the final image small.
  • COPY brings the compiled JAR from Maven's target/ directory into the container.
  • ENTRYPOINT tells Docker to run the JAR automatically when the container starts.
  • Save and exit nano.
  • Confirm both new files exist by running:
ls

You should see Dockerfile, pom.xml, src, and target listed in the output.

Don't see the Dockerfile?

Make sure you ran nano Dockerfile from the cicd-pipeline-app directory (not from src/ or your home directory). Run pwd to check your current location.

Help me figure out where my Dockerfile ended up

Push the project to GitHub

Jenkins needs to pull your code from a Git repository. You'll push everything to GitHub so the pipeline can clone it automatically during the Checkout stage.

  • Create a .gitignore file to keep build artifacts out of the repository by running:
nano .gitignore
  • Paste the following:
target/
*.class

Why ignore these files?

The target/ folder contains compiled output that Maven regenerates on every build. The *.class pattern catches any stray compiled Java files. Neither belongs in version control.

  • Save and exit nano.

Before pushing, you need to create a repository on GitHub to push to. You'll make it public so Jenkins can clone it without extra authentication.

  • Open your browser and go to https://github.com/new.
  • Enter cicd-pipeline-app as the Repository name.
  • Select Public for the visibility.
  • Leave all other options unchecked (no README, no .gitignore template, no license).
  • Click Create repository.

Why a public repository?

A public repository lets Jenkins clone your code via HTTPS without any credentials. If you use a private repository, you would need to create a GitHub Personal Access Token and add it as a "Username with password" credential in Jenkins.

GitHub will show a quick setup page with your repository URL. Keep that page open.

  • Back in your terminal, set your Git identity by running these commands:
git config --global user.email "[[GIT_EMAIL="your-email@example.com"]]"
git config --global user.name "[[GIT_NAME="your name"]]"
  • Initialize the Git repository and make your first commit by running these commands:
git init
git add .
git commit -m "Initial commit: Java/Maven project with JUnit tests"
  • Connect to your GitHub repository and push by running these commands:
git branch -M main
git remote add origin [[GITHUB_REPO_URL="https://github.com/your-username/cicd-pipeline-app.git"]]
git push -u origin main

What do these commands do?

  • git init creates a new Git repository in your project folder.
  • git add . stages all files (except those in .gitignore) for the commit.
  • git branch -M main renames the default branch to main.
  • git push -u origin main uploads everything to GitHub and sets main as the default upstream branch.
  • Refresh your GitHub repository page in the browser.

You should see your files listed: pom.xml, Dockerfile, .gitignore, and the src/ directory.

Push rejected or authentication error?

  • If you see remote: Repository not found, double-check that the repository URL matches exactly what GitHub shows on your repository page.
  • If you see an authentication prompt, GitHub requires a Personal Access Token for HTTPS pushes. Go to Settings > Developer settings > Personal access tokens > Tokens (classic) in GitHub. Generate a token with repo scope and use it as your password when prompted.
  • If you see error: remote origin already exists, remove the existing remote first with git remote remove origin, then re-add it.

Help me push my project to GitHub from Ubuntu

✔️ Awesome, I've got everything!

Great. Make sure all your files are saved and your GitHub repo shows the latest commit.

ⓧ I'd like to double check the full code

Here are all the files in your project. Compare them against yours to catch any differences.

package com.example;

public class App {
    public String greet(String name) {
        if (name == null || name.trim().isEmpty()) {
            return "Hello, World!";
        }
        return "Hello, " + name + "!";
    }

    public int add(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        App app = new App();
        System.out.println(app.greet("Jenkins Pipeline"));
        System.out.println("2 + 3 = " + app.add(2, 3));
    }
}
package com.example;

import static org.junit.Assert.*;
import org.junit.Test;

public class AppTest {

    @Test
    public void testGreetWithName() {
        App app = new App();
        assertEquals("Hello, Jenkins!", app.greet("Jenkins"));
    }

    @Test
    public void testGreetWithNull() {
        App app = new App();
        assertEquals("Hello, World!", app.greet(null));
    }

    @Test
    public void testGreetWithEmpty() {
        App app = new App();
        assertEquals("Hello, World!", app.greet("  "));
    }

    @Test
    public void testAdd() {
        App app = new App();
        assertEquals(5, app.add(2, 3));
    }

    @Test
    public void testAddNegative() {
        App app = new App();
        assertEquals(-1, app.add(2, -3));
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>cicd-pipeline-app</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>cicd-pipeline-app</name>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.sonarsource.scanner.maven</groupId>
                <artifactId>sonar-maven-plugin</artifactId>
                <version>4.0.0.4121</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.4.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/cicd-pipeline-app-1.0-SNAPSHOT.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
target/
*.class

Your Java project is on GitHub and ready for Jenkins to pull. Next up, you'll write the Jenkinsfile that tells the pipeline exactly how to build, test, and report on this code.

Write the Jenkinsfile with Build and Test Stages

Your Java project is on GitHub, and Jenkins is ready with the right plugins installed. The missing piece is telling Jenkins what to do with your code.

You define this in a Jenkinsfile, a text file that lives in your repository alongside your code. In this step, you will write a Jenkinsfile with three stages, create a Pipeline job in Jenkins, and watch your code go through the entire build-and-test flow automatically.

In this step, get ready to:
  • Create a Jenkinsfile with Checkout, Build, and Test stages.
  • Create a Pipeline job in Jenkins that reads the Jenkinsfile from GitHub.
  • Run the pipeline and verify all 5 JUnit tests pass.
Create the initial Jenkinsfile

A Declarative Pipeline defines your entire CI/CD workflow as code. Instead of clicking through a Jenkins UI to configure build steps, you write them in a Jenkinsfile that lives in your repository.

What is Pipeline-as-Code?

Pipeline-as-Code means your build, test, and deploy instructions are version-controlled alongside your application code. If someone changes a pipeline stage, that change shows up in Git history just like a code change.

This is the industry standard for CI/CD workflows. Every team member can see exactly what the pipeline does by reading the file.

  • Navigate to the root of your cicd-pipeline-app project folder on your Desktop by running:
cd ~/Desktop/cicd-pipeline-app
  • Create a new file called Jenkinsfile in the project root by running:
touch Jenkinsfile
  • Open Jenkinsfile in your text editor and paste the following code:
pipeline {
    // Run on any available Jenkins agent
    agent any

    environment {
        // Pull the SonarQube token from Jenkins credentials store
        SONAR_TOKEN = credentials('sonarqube-token')
        // Name for the Docker image built later in the pipeline
        DOCKER_IMAGE = "cicd-pipeline-app"
    }

    stages {
        // Stage 1: Pull the latest code from GitHub
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/cicd-pipeline-app.git'
            }
        }

        // Stage 2: Compile the project and package it into a JAR
        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        // Stage 3: Run all JUnit tests
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                // Always publish test results, even if tests fail
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }
    }
}

What does this Jenkinsfile do?

  • agent any tells Jenkins to run the pipeline on whichever agent is available. Since you only have one Jenkins instance, it runs there.
  • The environment block loads the SonarQube token you stored as a Jenkins credential in Step 2. It also defines the Docker image name for a later stage.
  • The Checkout stage clones your GitHub repository so Jenkins has the source code to work with.
  • The Build stage runs mvn clean package -DskipTests to compile the Java code and package it into a JAR file. The -DskipTests flag skips tests here because the next stage handles them separately.
  • The Test stage runs mvn test to execute all JUnit tests. The post > always block publishes test results to Jenkins so you can see them in the UI, even if a test fails.
  • Save Jenkinsfile.
  • Commit and push the Jenkinsfile to GitHub by running these commands:
git add Jenkinsfile
git commit -m "Add Jenkinsfile with build and test stages"
git push origin main
  • Confirm the push succeeded. You should see output ending with a line like main -> main.

Push failed or asked for credentials?

  • If Git asks for a username and password, GitHub no longer accepts passwords for HTTPS pushes. You need a Personal Access Token. Go to GitHub > Settings > Developer settings > Personal access tokens > Tokens (classic), generate a token with repo scope, and use it as your password when prompted.
  • If you see fatal: not a git repository, make sure you are inside the cicd-pipeline-app folder.

Still stuck? Ask for help.

✔️ Awesome, I've got everything!

Great. Double check you have saved your Jenkinsfile and pushed it to GitHub.

ⓧ I'd like to double check the full code

Here is the complete Jenkinsfile you should have at this point.

pipeline {
    agent any

    environment {
        SONAR_TOKEN = credentials('sonarqube-token')
        DOCKER_IMAGE = "cicd-pipeline-app"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/cicd-pipeline-app.git'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }
    }
}
Create the Pipeline job in Jenkins

The Jenkinsfile is on GitHub, but Jenkins does not know about it yet. You need to create a Pipeline job that tells Jenkins where to find your repository and Jenkinsfile.

  • Open your Jenkins dashboard in a browser at http://localhost:8080.
  • Click New Item in the left sidebar.
  • Enter cicd-pipeline-app as the item name.
  • Select Pipeline from the list of project types.
  • Click OK.

This opens the job 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 GitHub HTTPS URL:
https://github.com/[[GITHUB_USERNAME="your GitHub username"]]/cicd-pipeline-app.git
  • In the Branch Specifier field, enter */main.
  • In the Script Path field, confirm it says Jenkinsfile. This is the default value.
  • Click Save at the bottom of the page.

What does Pipeline script from SCM mean?

SCM stands for Source Code Management (in this case, Git). By selecting this option, you tell Jenkins to fetch the pipeline definition from your repository instead of pasting it into the Jenkins UI.

This keeps your pipeline version-controlled. Every change to the pipeline goes through the same Git workflow as your application code.

Using a private repository?

If your GitHub repository is private, Jenkins needs authentication to clone it. Create a GitHub Personal Access Token, then add it in Jenkins as a Username with password credential. Use your GitHub username as the username and the token as the password.

Select this credential in the Credentials dropdown on the Pipeline configuration page. Public repositories do not need this step.

Run the pipeline and verify

The Pipeline job is configured. Time to trigger the first build and see your three stages run.

  • On the cicd-pipeline-app job page, click Build Now in the left sidebar.

A new build appears in the Build History panel on the left. After a moment, the stage view loads at the top of the page showing your three stages.

  • Wait for all three stages to complete. You should see green checkmarks next to Checkout, Build, and Test.
  • Click the build number (e.g. #1) in the Build History panel.
  • Click Test Result in the left sidebar. You should see 5 tests listed, all passing with 0 failures.

Why publish JUnit test results?

The junit 'target/surefire-reports/*.xml' line in the Jenkinsfile tells Jenkins to read the XML test reports that Maven generates. Jenkins uses these reports to show test trends, individual test results, and failure details directly in the build page.

The always keyword ensures test results are published even when tests fail. Without it, a test failure would skip the publishing step and you would lose visibility into which tests broke.

Build failed at the Checkout stage?

  • If you see Error cloning remote repo or Permission denied, verify that Git is installed by running git --version on your system.
  • Check the Jenkins workspace ownership by running ls -ld /var/lib/jenkins/workspace. It should show jenkins jenkins as the owner and group. If not, fix it with sudo chown -R jenkins:jenkins /var/lib/jenkins.
  • Confirm the Git safe directory is configured for the Jenkins user by running sudo -u jenkins git config --global --get-all safe.directory. It should output *.

Help me troubleshoot my Jenkins Checkout stage failure.

Build failed with mvn: command not found?

This means Maven is not on the system PATH that Jenkins uses. Verify Maven is installed by running mvn -version in your terminal. If Maven is not found, go back to Step 1 and install it with sudo apt install maven, then restart Jenkins with sudo systemctl restart jenkins.

Help me fix the mvn command not found error in Jenkins.

Your pipeline is building and testing code automatically. Next up, you will wire in SonarQube to scan your code for bugs and vulnerabilities before it ever reaches production.

Integrate SonarQube for Static Code Analysis

Your pipeline now compiles Java code, runs all five unit tests, and reports the results automatically. But passing tests only proves the code works. It doesn't tell you whether the code is well-written, secure, or maintainable.

Static code analysis scans source code for bugs, vulnerabilities, and bad patterns without executing it. In this step, you'll wire SonarQube into your Jenkins pipeline so every build gets a quality check before it can pass.

In this step, get ready to:
  • Configure a SonarQube webhook so Jenkins receives analysis results.
  • Add SonarQube Analysis and Quality Gate stages to your Jenkinsfile.
  • Run the pipeline and review your code quality dashboard.
Configure the SonarQube webhook

A webhook is a callback URL that one service uses to notify another when something happens. SonarQube needs a webhook to tell Jenkins when an analysis is complete.

Without this webhook, the waitForQualityGate step in your pipeline will hang forever, waiting for a callback that never arrives.

Why can't the webhook use localhost?

SonarQube runs inside a Docker container. Inside that container, localhost refers to the container itself, not your Ubuntu machine where Jenkins is running.

You need to use your machine's actual IP address so SonarQube's container can reach Jenkins on your host.

  • Find your machine's IP address by running this command:
hostname -I | awk '{print $1}'

You should see an IP address like 192.168.x.x printed in your terminal.

  • Copy the IP address from the output.
  • Open http://localhost:9000 in your browser.
  • Click Administration in the top navigation bar.
  • Click Configuration, then select Webhooks from the dropdown.
  • Click Create.
  • Enter Jenkins in the Name field.
  • In the URL field, paste the following URL with your own IP address:
http://[[HOST_IP="your IP address from the previous command"]]:8080/sonarqube-webhook/

The trailing slash matters

The URL must end with /sonarqube-webhook/ including the trailing slash. Without it, Jenkins will not receive the callback.

  • Click Create.

You should see the Jenkins webhook listed on the Webhooks page with the URL you entered.

Don't see the Webhooks option?

Make sure you are logged in as the admin user. Only administrators can see the Administration menu in SonarQube.

help me find the Webhooks configuration in SonarQube

Add the SonarQube Analysis and Quality Gate stages to the Jenkinsfile

Your Jenkinsfile currently has three stages: Checkout, Build, and Test. Now you'll add two more stages that send your code to SonarQube for analysis and then check whether it passes the quality gate.

  • Open the Jenkinsfile in your cicd-pipeline-app project folder.
  • Find the closing brace } of the stage('Test') block. It looks like this:
        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }
  • Add the following two stages directly after the stage('Test') block, inside the stages { } section:
        stage('SonarQube Analysis') {
            steps {
                // Inject SonarQube environment variables using the 'SonarQube' server config
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                // Wait for SonarQube to return results, fail the build if quality gate fails
                timeout(time: 2, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

What do these two stages do?

The SonarQube Analysis stage runs mvn sonar:sonar inside a withSonarQubeEnv block. This block automatically injects the SonarQube server URL and authentication token you configured in Jenkins, so Maven knows where to send the analysis.

The Quality Gate stage pauses the pipeline and waits for SonarQube to send back its verdict via the webhook you just configured. If the code fails the quality gate, abortPipeline: true tells Jenkins to fail the entire build.

The timeout wrapper prevents the pipeline from hanging indefinitely if SonarQube doesn't respond within 2 minutes.

  • Save the Jenkinsfile.
  • Commit and push the updated Jenkinsfile to GitHub by running these commands:
cd ~/Desktop/cicd-pipeline-app
git add Jenkinsfile
git commit -m "Add SonarQube analysis and quality gate stages"
git push origin main

Git push rejected or asking for credentials?

If Git asks for a username and password, GitHub no longer accepts password authentication. Use a Personal Access Token as your password, or set up SSH authentication.

If the push is rejected because of remote changes, run git pull --rebase origin main first, then try pushing again.

help me push my Jenkinsfile changes to GitHub

✔️ Awesome, I've got everything!

Make sure you've saved your Jenkinsfile and pushed it to GitHub before moving on.

ⓧ I'd like to double check the full code

Here is the complete Jenkinsfile as it should look after adding the two new stages.

pipeline {
    agent any

    environment {
        SONAR_TOKEN = credentials('sonarqube-token')
        DOCKER_IMAGE = "cicd-pipeline-app"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/your-username/cicd-pipeline-app.git'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 2, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }
    }
}
Run the pipeline and review SonarQube results

The updated Jenkinsfile is on GitHub. Time to trigger a build and see your five-stage pipeline in action.

  • Open Jenkins in your browser at http://localhost:8080.
  • Click on your cicd-pipeline-app pipeline job.
  • Click Build Now in the left sidebar.
  • Wait for the build to complete. You should see all five stages (Checkout, Build, Test, SonarQube Analysis, Quality Gate) with green checkmarks in the Stage View.

The Quality Gate stage may take a moment as it waits for SonarQube to finish analyzing and send the webhook callback.

Quality Gate stage stuck or timed out?

This almost always means the SonarQube webhook is misconfigured. Double check these common causes:

  • The webhook URL must use your machine's actual IP address, not localhost. Run hostname -I | awk '{print $1}' to confirm your IP.
  • The URL must end with a trailing slash: /sonarqube-webhook/ not /sonarqube-webhook.
  • Make sure Jenkins is reachable on port 8080 from the SonarQube container. Check your firewall is not blocking connections.

help me fix the Quality Gate stage timing out in my Jenkins pipeline

Now review the analysis results in SonarQube.

  • Open http://localhost:9000 in your browser.
  • Click on your cicd-pipeline-app project in the Projects list.

You should see the SonarQube analysis dashboard showing metrics for bugs, vulnerabilities, code smells, and coverage. The quality gate status should display Passed.

What is a quality gate?

A quality gate is a set of threshold conditions that code must meet before it's considered acceptable. SonarQube's default quality gate checks for new bugs, new vulnerabilities, code smells, and test coverage percentages.

If any threshold is breached, the gate fails and your pipeline stops. This prevents bad code from progressing further in the delivery process.

Don't see your project in SonarQube?

Make sure the SonarQube Analysis stage completed successfully in Jenkins. Click into the build, select Console Output, and look for lines mentioning ANALYSIS SUCCESSFUL. If you see errors, verify that SonarQube is running with docker ps.

help me debug why my SonarQube project is not showing analysis results

Your pipeline now enforces code quality standards automatically on every build. Next up, you'll add Docker containerization and Slack notifications to complete the pipeline.

Add the Docker Build Stage and Slack Notifications

Your pipeline now builds, tests, and analyzes your code automatically. The next piece of a production pipeline is packaging your application and telling your team about it.

In this step, you will add a Docker Build stage that packages your Java app into a container image. You will also wire up Slack notifications so every build result lands in a team channel in real time.

In this step, get ready to:
  • Add a Docker Build stage to the Jenkinsfile that creates container images.
  • Create a Slack workspace and app, then connect it to Jenkins.
  • Add Slack notifications for build success and failure, then run the complete pipeline.
Add the Docker Build stage to your Jenkinsfile

Your pipeline already compiles a JAR file during the Build stage. The Docker Build stage takes that JAR and packages it into a container image using the Dockerfile you created earlier.

  • Open your Jenkinsfile in your local project directory.
  • Find the closing } of the Quality Gate stage.
  • Add the following Docker Build stage directly after it, before the closing } of the stages block:
        stage('Docker Build') {
            steps {
                script {
                    // Build image tagged with the Jenkins build number
                    docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                    // Also tag as latest for convenience
                    docker.build("${DOCKER_IMAGE}:latest")
                }
            }
        }

What does this code do?

The docker.build() method comes from the Docker Pipeline plugin you installed earlier. It reads the Dockerfile in your repo and builds a container image.

Tagging with ${env.BUILD_NUMBER} gives each build a unique, traceable image. Tagging with latest means downstream systems can always pull the most recent version. The DOCKER_IMAGE variable was already defined in the environment block of your Jenkinsfile.

  • Save Jenkinsfile.
  • Commit and push the updated Jenkinsfile by running these commands:
git add Jenkinsfile
git commit -m "Add Docker Build stage"
git push origin main

Push rejected or authentication error?

Make sure you are in the correct project directory (cicd-pipeline-app). If Git asks for credentials, use your GitHub username and a Personal Access Token (not your password).

Help me troubleshoot my git push failure.

✔️ Awesome, I've got everything!

Great. Double check you have saved your Jenkinsfile and pushed it to GitHub.

ⓧ I'd like to double check the full code

Here is the full Jenkinsfile at this point. It has 6 stages and no post block yet.

pipeline {
    agent any

    environment {
        SONAR_TOKEN = credentials('sonarqube-token')
        DOCKER_IMAGE = "cicd-pipeline-app"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/your-username/cicd-pipeline-app.git'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 2, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Docker Build') {
            steps {
                script {
                    docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                    docker.build("${DOCKER_IMAGE}:latest")
                }
            }
        }
    }
}
Create a Slack workspace and connect it to Jenkins

A CI/CD pipeline needs to communicate results to the team. You will create a free Slack workspace, build a custom Slack app, and connect it to Jenkins so the pipeline can post build notifications automatically.

Create your Slack workspace and channel
  • Open https://slack.com/get-started#/createnew in your browser.
  • Enter your email address and follow the prompts to create a new workspace.
  • When asked for a workspace name, enter something like Jenkins CI.
  • Once inside your new workspace, create a channel named #jenkins-builds by clicking the + next to Channels in the sidebar.
Create a Slack app from a manifest
  • Open https://api.slack.com/apps in a new browser tab.
  • Click Create New App.
  • Click From an app manifest.
  • Select your new workspace from the dropdown, then click Next.
  • Delete the example manifest that Slack provides.
  • Click YAML at the top of the editor.
  • Paste the following manifest into the text box:
display_information:
  name: Jenkins CI
features:
  bot_user:
    display_name: Jenkins CI
    always_online: true
oauth_config:
  scopes:
    bot:
      - channels:read
      - chat:write
      - chat:write.customize
      - files:write
      - reactions:write
      - users:read
      - users:read.email
      - groups:read
settings:
  org_deploy_enabled: false
  socket_mode_enabled: false
  token_rotation_enabled: false

What are these bot scopes?

Each scope grants the bot a specific permission. chat:write lets it post messages. channels:read lets it see which channels exist. The remaining scopes cover features like customizing the bot name and reading user info for richer notifications.

  • Click Next.
  • Review the permissions summary, then click Create.
  • Click Install to Workspace.
  • Click Allow to grant the permissions.
Copy the bot token and add it to Jenkins
  • In the Slack app settings sidebar, click OAuth & Permissions.
  • Copy the Bot User OAuth Token (it starts with xoxb-). Keep this token safe.
  • In Jenkins, go to Manage Jenkins, then Credentials.
  • Click (global) under Stores scoped to Jenkins.
  • Click Add Credentials.
  • Set Kind to Secret text.
  • Paste the Bot User OAuth Token into the Secret field.
  • Set the ID to slack-token.
  • Click Create.
Configure the Slack plugin in Jenkins
  • Go to Manage Jenkins, then System.
  • Scroll down to the Slack section.
  • In the Workspace field, enter the name of your Slack workspace (the subdomain, not the full URL).
  • In the Credential dropdown, select slack-token.
  • Tick the Custom slack app bot user checkbox.
  • Click Save.
Invite the bot to your channel
  • Go back to your Slack workspace.
  • Open the #jenkins-builds channel.
  • Type /invite @Jenkins CI in the message box and press Enter to invite the bot.

You should see a message confirming the bot has joined the channel.

Bot not showing up in the invite list?

Make sure you clicked Install to Workspace in the Slack app settings. The bot does not appear until the app is installed. Also confirm the app name matches what you are typing in the invite command.

Help me troubleshoot my Slack bot not appearing in my workspace.

Add Slack notifications and run the final pipeline

The last piece is telling the pipeline to send a Slack message after every build. A post block at the pipeline level runs after all stages complete, regardless of which stage failed.

  • Open your Jenkinsfile.
  • Find the closing } of the stages block (after the Docker Build stage).
  • Add the following post block after it, inside the pipeline block:
    post {
        success {
            // Send a green notification when the build passes
            slackSend(
                channel: '#jenkins-builds',
                color: 'good',
                message: "SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
            )
        }
        failure {
            // Send a red notification when the build fails
            slackSend(
                channel: '#jenkins-builds',
                color: 'danger',
                message: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
            )
        }
    }

What does this code do?

The post block runs after all stages finish. The success condition triggers only when every stage passes. The failure condition triggers if any stage fails.

The slackSend step uses the Slack Notification plugin to post a message. The color parameter controls the message sidebar color: good for green and danger for red. The message includes a clickable link back to the Jenkins build.

  • Save Jenkinsfile.
  • Commit and push the final Jenkinsfile by running:
git add Jenkinsfile
git commit -m "Add Slack notifications"
git push origin main
  • In Jenkins, open your cicd-pipeline-app pipeline job.
  • Click Build Now to trigger the build.

Wait for all 6 stages to complete. You should see green checkmarks on Checkout, Build, Test, SonarQube Analysis, Quality Gate, and Docker Build.

  • Switch to your Slack workspace and open the #jenkins-builds channel.

You should see a green SUCCESS message from the Jenkins CI bot with your job name, build number, and a link back to Jenkins.

No Slack message arrived?

Check the Jenkins build console output for Slack-related errors. Common causes include:

  • The bot was not invited to #jenkins-builds. Run /invite @Jenkins CI in the channel.
  • The workspace name in Manage Jenkins > System > Slack does not match your actual Slack subdomain.
  • The Custom slack app bot user checkbox is not ticked.

Help me debug why my Slack notification is not arriving from Jenkins.

  • Verify your Docker images were created by running:
docker images | grep cicd-pipeline-app

You should see two entries: one tagged with the build number and one tagged latest.

Docker Build stage failed?

If you see a "permission denied" error related to /var/run/docker.sock, the Jenkins user does not have Docker access. Run sudo usermod -aG docker jenkins and restart Jenkins with sudo systemctl restart jenkins. Then trigger the build again.

Help me fix the Docker Build stage permission error in Jenkins.

✔️ Awesome, I've got everything!

Great. Double check you have saved Jenkinsfile and pushed it to GitHub. Your pipeline now has 6 stages and a post block for Slack notifications.

ⓧ I'd like to double check the full code

Here is the complete final Jenkinsfile:

pipeline {
    agent any

    environment {
        SONAR_TOKEN = credentials('sonarqube-token')
        DOCKER_IMAGE = "cicd-pipeline-app"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: 'main', url: 'https://github.com/your-username/cicd-pipeline-app.git'
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean package -DskipTests'
            }
        }

        stage('Test') {
            steps {
                sh 'mvn test'
            }
            post {
                always {
                    junit 'target/surefire-reports/*.xml'
                }
            }
        }

        stage('SonarQube Analysis') {
            steps {
                withSonarQubeEnv('SonarQube') {
                    sh 'mvn sonar:sonar'
                }
            }
        }

        stage('Quality Gate') {
            steps {
                timeout(time: 2, unit: 'MINUTES') {
                    waitForQualityGate abortPipeline: true
                }
            }
        }

        stage('Docker Build') {
            steps {
                script {
                    docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                    docker.build("${DOCKER_IMAGE}:latest")
                }
            }
        }
    }

    post {
        success {
            slackSend(
                channel: '#jenkins-builds',
                color: 'good',
                message: "SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
            )
        }
        failure {
            slackSend(
                channel: '#jenkins-builds',
                color: 'danger',
                message: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} (<${env.BUILD_URL}|Open>)"
            )
        }
    }
}

Your pipeline is now complete. It checks out code, builds it, runs tests, analyzes code quality, packages a Docker image, and notifies your team on Slack. Next up, you will take this even further with a Secret Mission.

Secret mission

Add a GitHub Webhook Trigger

Your pipeline works, but you still have to click Build Now every time. What if Jenkins could detect code changes on its own and start the pipeline automatically? In this secret mission, you will wire up a GitHub webhook to trigger your pipeline on every push.

Before you go

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 on free, open-source tools, so there are no ongoing financial costs. The main consideration is that the SonarQube container uses roughly 2GB of RAM while running.

Resources you used:

  • SonarQube Docker container (running on port 9000, uses ~2GB RAM). Free, no ongoing cost.
  • Docker images (SonarQube image and your app image). Takes disk space but no cost.
  • Jenkins pipeline job and credentials. No cost.
  • Slack workspace and app. Free tier, no cost.
  • System kernel parameters (vm.max_map_count, fs.file-max). Harmless to leave in place.
  • Git safe.directory config for the jenkins user. Harmless to leave in place.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend this pipeline with follow-on projects like Nexus or Ansible.

SonarQube uses approximately 2GB of RAM while running. If your system has the resources, leaving everything in place lets you jump straight into your next project.

Pause - I'll come back to this later

Stop the SonarQube container to free up memory. Your data persists in the Docker volumes, so you can pick up right where you left off.

  • Stop the SonarQube container by running:
docker stop sonarqube
  • When you are ready to resume, restart it by running:
docker start sonarqube

All your SonarQube data, tokens, and project history persist in the Docker volumes. Jenkins, your pipeline job, and credentials all remain intact.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow these steps to clean up everything created during this project.

  • Stop and remove the SonarQube container and its data by running these commands:
# Stop and remove the SonarQube container
docker stop sonarqube
docker rm sonarqube

# Remove the SonarQube Docker image
docker rmi sonarqube:community

# Remove SonarQube data volumes
docker volume rm sonarqube_data sonarqube_logs sonarqube_extensions
  • Remove your application Docker images by running:
# Remove your app Docker images (list them first)
docker images | grep cicd-pipeline-app
docker rmi cicd-pipeline-app:latest

# Remove any build-number tagged images
docker images | grep cicd-pipeline-app | awk '{print $1":"$2}' | xargs docker rmi
  • Clean up the Maven cache and dangling Docker resources by running:
# Remove the Maven cache if you no longer need it
rm -rf ~/.m2/repository

# Clean up dangling Docker images and build cache
docker system prune -f
docker builder prune -f
  • Optionally, remove the SonarQube kernel configuration by running:
# Remove the SonarQube kernel config (optional, harmless to leave)
sudo rm /etc/sysctl.d/99-sonarqube.conf
sudo sysctl --system
  • In the Jenkins UI, navigate to the pipeline job and click Delete Pipeline to remove the job.
  • Go to Manage Jenkins > Credentials and delete the SonarQube and Slack tokens.
  • Optionally, remove the Slack app by going to https://api.slack.com/apps, selecting your Jenkins app, then clicking Settings > Delete App.

Mission accomplished

That's a Wrap!

You just built a professional-grade CI/CD pipeline from scratch. That is a serious DevOps skill.

You've learned how to:

  • Defined a complete Jenkins Declarative Pipeline in a Jenkinsfile with five automated stages that build, test, analyze, and containerize a Java application.
  • Integrated SonarQube for static code analysis with quality gate enforcement that blocks bad code from progressing through the pipeline.
  • Built Docker images as deployment artifacts and wired up Slack notifications for real-time build status alerts.
  • Secret Mission: Configured a GitHub webhook to trigger your pipeline automatically on every code push.

Ready to quiz yourself?