Skip to content
DBDeependra Bhatta~/notes
CI/CD#java · #maven · #jenkins · #nexus · #sonarqube

SonarQube and Nexus in a Jenkins Pipeline

Install SonarQube and Sonatype Nexus, define a quality gate, and extend a Jenkins pipeline to scan Java code and publish each WAR build to a Nexus repository.

· updated · 15 min read
ON THIS PAGE

A pipeline that ships code without inspecting it, or without keeping the exact artifact it deployed, makes releases hard to trust and hard to roll back. This guide adds two stages to a Java pipeline: SonarQube analysis judged by a quality gate, and an upload of the built .war file to Sonatype Nexus, so every build is both inspected and versioned outside Jenkins. Both servers run on Vagrant VMs and are installed by script.

MachineIPRuns
Jenkins192.168.56.5Jenkins (port 8080)
SonarQube192.168.56.6SonarQube (port 9000)
Nexus192.168.56.5Nexus (port 8081), on the Jenkins VM

Prerequisites

  • The Jenkins pipeline from Jenkins pipelines, building a Maven web app.
  • Ubuntu VMs reachable from Jenkins, as in the table above.
  • A Maven build that produces Surefire test results, a JaCoCo XML report and a Checkstyle report.

SonarQube in short

SonarQube has two parts:

  • SonarQube server: the web dashboard. It stores analysis results, applies quality gates and needs a database (PostgreSQL here) and an embedded Elasticsearch.
  • SonarScanner: a command-line client that runs during the build, analyzes the source code and uploads a report to the server.

A quality profile is the set of rules used to analyze code in one language. A quality gate is a set of pass or fail conditions on the results, for example "coverage on new code is at least 80%". If a project fails its gate, it should not be released.

Install SonarQube

Install SonarQube Community Build on an Ubuntu VM with the script below. It sets the kernel and user limits SonarQube needs, installs Java 17 and PostgreSQL, creates the database, installs SonarQube as a systemd service and puts Nginx in front of it.

SHinstall-sonarqube.sh
#!/bin/bash
set -e
 
echo "=== SonarQube Installation Started ==="
 
# Kernel limits for SonarQube's embedded Elasticsearch
cat <<EOT > /etc/sysctl.d/99-sonarqube.conf
vm.max_map_count=524288
fs.file-max=131072
EOT
sysctl --system
 
# User limits for the sonar user
cat <<EOT > /etc/security/limits.d/99-sonarqube.conf
sonar   -   nofile   131072
sonar   -   nproc    8192
EOT
 
# Install Java 17
apt-get update -y
apt-get install openjdk-17-jdk wget curl unzip ca-certificates -y
java -version
 
# Install PostgreSQL from the PostgreSQL apt repository
install -d /usr/share/postgresql-common/pgdg
curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list
apt-get update -y
apt-get install postgresql postgresql-contrib -y
systemctl enable postgresql
systemctl start postgresql
 
# Create the database and its user (lab password, change it)
sudo -u postgres psql -c "CREATE USER sonar WITH ENCRYPTED PASSWORD 'admin123';"
sudo -u postgres psql -c "CREATE DATABASE sonarqube OWNER sonar;"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE sonarqube TO sonar;"
 
# Install SonarQube
mkdir -p /opt
cd /opt
curl -O https://binaries.sonarsource.com/Distribution/sonarqube/sonarqube-25.9.0.112764.zip
unzip -o sonarqube-25.9.0.112764.zip
mv sonarqube-25.9.0.112764 sonarqube
 
# Create the sonar user and give it the install folder
groupadd sonar || true
useradd -c "SonarQube - User" -d /opt/sonarqube/ -g sonar sonar || true
chown -R sonar:sonar /opt/sonarqube
 
# Point SonarQube at PostgreSQL
cp /opt/sonarqube/conf/sonar.properties /root/sonar.properties_backup
cat <<EOT > /opt/sonarqube/conf/sonar.properties
sonar.jdbc.username=sonar
sonar.jdbc.password=admin123
sonar.jdbc.url=jdbc:postgresql://localhost/sonarqube
sonar.web.host=0.0.0.0
sonar.web.port=9000
sonar.web.javaAdditionalOpts=-server
sonar.search.javaOpts=-Xmx512m -Xms512m -XX:+HeapDumpOnOutOfMemoryError
sonar.log.level=INFO
sonar.path.logs=logs
EOT
 
# systemd service
cat <<EOT > /etc/systemd/system/sonarqube.service
[Unit]
Description=SonarQube service
After=syslog.target network.target
 
[Service]
Type=simple
User=sonar
Group=sonar
ExecStart=/opt/sonarqube/bin/linux-x86-64/sonar.sh console
Restart=always
LimitNOFILE=131072
LimitNPROC=8192
 
[Install]
WantedBy=multi-user.target
EOT
 
systemctl daemon-reload
systemctl enable sonarqube
systemctl start sonarqube
 
echo "=== Waiting for SonarQube to start (this may take 1-3 minutes) ==="
until curl -s http://127.0.0.1:9000 > /dev/null; do
  sleep 10
  echo "Still starting..."
done
echo "SonarQube is now running!"
 
# Nginx reverse proxy on port 80
apt-get install nginx -y
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default
cat <<EOT > /etc/nginx/sites-available/sonarqube
server {
    listen      80;
    server_name _;
 
    access_log  /var/log/nginx/sonar.access.log;
    error_log   /var/log/nginx/sonar.error.log;
 
    proxy_buffers 16 64k;
    proxy_buffer_size 128k;
 
    location / {
        proxy_pass  http://127.0.0.1:9000;
        proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
        proxy_redirect off;
 
        proxy_set_header    Host            \$host;
        proxy_set_header    X-Real-IP       \$remote_addr;
        proxy_set_header    X-Forwarded-For \$proxy_add_x_forwarded_for;
        proxy_set_header    X-Forwarded-Proto http;
    }
}
EOT
 
ln -s /etc/nginx/sites-available/sonarqube /etc/nginx/sites-enabled/sonarqube
systemctl enable nginx
systemctl restart nginx
 
ufw allow 80,9000,9001/tcp || true
 
# On Vagrant the second address is the private network IP
SERVER_IP=$(hostname -I | awk '{print $2}')
 
echo "======================================================"
echo " SonarQube installation completed successfully!"
echo " Access it via:  http://$SERVER_IP:9000/"
echo " Default login: admin / admin"
echo "======================================================"

The limit values follow the SonarQube Linux pre-installation guide. The \$ in the Nginx block stops Bash from expanding Nginx variables inside the heredoc. SonarQube asks you to change the admin password at the first login.

Open http://<server-ip>:9000 in a browser:

SonarQube Community Build at 192.168.56.6:9000 asking how to create a project, with import and local project options

Quality gates

Create a custom gate under Quality Gates; the lab gate is named demoo-dipen. Like the built-in Sonar way gate, it follows "Clean as You Code": the conditions apply to new code only, so old code does not block today's change.

SonarQube Quality Gates page with custom gate demoo-dipen and the built-in Sonar way default

Conditions on new code: issues above 0, hotspots reviewed below 100%, coverage below 80%, duplicated lines above 3%

The gate fails if any of these is true on new code:

MetricFails whenMeaning
Issuesgreater than 0Bugs, vulnerabilities or code smells (hard-to-maintain code)
Security Hotspots Reviewedless than 100%Security-sensitive code that a person must review and mark safe
Coverageless than 80%Share of code run by unit tests
Duplicated Lines (%)greater than 3%Copy-pasted blocks

Projects also receive A to E ratings for security, reliability and maintainability. These ratings can be used as conditions, and each project can be assigned its own gate.

Administration

Administration holds the server settings: users, groups and permissions under Security, authentication (LDAP, SAML, GitHub, GitLab), email, webhooks, default quality gates and profiles, plugins in Marketplace, and system health and logs under System.

SonarQube Administration menu with Configuration, Security, Projects, System and Marketplace, and the General Settings list

Connect Jenkins to SonarQube

The scanner runs on the machine that builds the code, so Jenkins needs the scanner and the server address.

Install the plugin

In Manage Jenkins → Plugins → Available plugins, install SonarQube Scanner.

Jenkins plugin search for sonar with SonarQube Scanner 2.18 selected for install

Add the scanner as a tool

In Manage Jenkins → Tools, click Add SonarQube Scanner, name it, tick Install automatically and pick a version. Jenkins downloads the scanner the first time a job asks for it.

Jenkins Tools page with SonarQube Scanner named sonar7.2, installed automatically from Maven Central, version 7.2.0.5079

The pipeline loads the tool by that name:

GRVJenkinsfile (excerpt)
environment {
    scannerHome = tool 'sonar7.2'   // name from Manage Jenkins → Tools
}

Add the SonarQube server and token

In Manage Jenkins → System → SonarQube installations, add a server. The Name is what the pipeline uses in withSonarQubeEnv.

Jenkins System page: SonarQube installation named sonar with server URL http://192.168.56.6:9000/

Jenkins needs a token to talk to SonarQube. In SonarQube, go to Administration → Security → Users and open the Tokens menu for your user:

SonarQube Administration Security Users page with the Tokens menu for the Administrator user

Enter a name such as Jenkins, choose an expiry and click Generate. Copy the token right away; SonarQube shows it only once.

Back in Jenkins, add the token as a Secret text credential:

Jenkins Add Credentials form: kind Secret text, ID sonarqube, token pasted into the Secret field

Then select it as the Server authentication token and save:

SonarQube server settings in Jenkins with the new secret text selected as the server authentication token

The analysis stage

GRVJenkinsfile (excerpt)
stage('Sonar Analysis') {
    steps {
        // 'sonar' is the server name from Manage Jenkins → System
        withSonarQubeEnv('sonar') {
            sh '''${scannerHome}/bin/sonar-scanner \
                -Dsonar.projectKey=java-tomcat-sample \
                -Dsonar.projectName=java-tomcat-sample \
                -Dsonar.projectVersion=4.0 \
                -Dsonar.sources=src/ \
                -Dsonar.junit.reportPaths=target/surefire-reports/ \
                -Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml \
                -Dsonar.java.checkstyle.reportPaths=target/checkstyle-result.xml'''
        }
    }
}

withSonarQubeEnv passes the server URL and token to the scanner. The properties:

PropertyValue hereMeaning
sonar.projectKeyjava-tomcat-sampleUnique project ID in SonarQube; here it matches the Maven artifactId.
sonar.projectNamejava-tomcat-sampleName shown in the dashboard
sonar.projectVersion4.0Version label shown in the dashboard
sonar.sourcessrc/Folder with the source code
sonar.junit.reportPathstarget/surefire-reports/Unit test results from Maven Surefire
sonar.coverage.jacoco.xmlReportPathstarget/site/jacoco/jacoco.xmlJaCoCo coverage report (XML)
sonar.java.checkstyle.reportPathstarget/checkstyle-result.xmlCheckstyle results from the earlier stage

The artifactId in pom.xml:

XMLpom.xml
<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>java-tomcat-sample</artifactId>
  <packaging>war</packaging>
  <version>1.0.1</version>
  <name>hello Maven Webapp</name>

The result

The build log ends with a link to the results:

Jenkins console: SonarScanner analysis successful with dashboard link for java-tomcat-sample, then Finished SUCCESS

SonarQube overview for java-tomcat-sample: quality gate passed, 3 reliability issues, coverage not computed

The gate passed even with three reliability issues and no coverage. That is because the conditions apply to new code, and the first analysis has no new code to compare against. Jenkins also shows the gate status on the job page:

Jenkins job java_app showing the archived WAR and SonarQube Quality Gate passed for java-tomcat-sample

Artifact repositories

Jenkins keeps archived artifacts with each build, but that is not a good long-term store. An artifact repository keeps every build's output (.war, .jar, Docker images, npm packages) with a version, so other teams and deploy jobs can download exactly the build they need.

ToolNotes
JFrog ArtifactorySupports many package formats; commercial, with a free tier
Apache ArchivaOpen source, focused on Maven
Sonatype Nexus RepositoryMaven, Docker, npm and more; free Community Edition

This pipeline uses Sonatype Nexus Community Edition.

Install Sonatype Nexus

SHinstall-nexus.sh
#!/bin/bash
set -e
 
NEXUS_VERSION="3.84.1-01"
NEXUS_URL="https://download.sonatype.com/nexus/3/nexus-${NEXUS_VERSION}-linux-x86_64.tar.gz"
NEXUS_DIR="nexus-$NEXUS_VERSION"
 
echo "Starting Nexus installation..."
 
echo "Installing Java and dependencies..."
sudo apt update
sudo apt install openjdk-17-jdk wget -y
 
echo "Creating directories..."
sudo mkdir -p /opt/nexus/
sudo mkdir -p /tmp/nexus/
 
echo "Downloading Nexus..."
cd /tmp/nexus/
sudo wget $NEXUS_URL -O nexus.tar.gz
echo "Extracting Nexus..."
sudo tar xzvf nexus.tar.gz
sudo rm -f /tmp/nexus/nexus.tar.gz
 
# The archive holds two folders: nexus-<version> (the app) and sonatype-work (the data)
echo "Installing Nexus to /opt/nexus/"
sudo rsync -avzh /tmp/nexus/ /opt/nexus/
 
echo "Creating nexus user..."
if id "nexus" &>/dev/null; then
    echo "User nexus already exists."
else
    sudo useradd nexus
fi
 
echo "Setting permissions..."
sudo chown -R nexus:nexus /opt/nexus
 
echo "Creating systemd service..."
cat <<EOT | sudo tee /etc/systemd/system/nexus.service > /dev/null
[Unit]
Description=nexus service
After=network.target
 
[Service]
Type=forking
LimitNOFILE=65536
ExecStart=/opt/nexus/$NEXUS_DIR/bin/nexus start
ExecStop=/opt/nexus/$NEXUS_DIR/bin/nexus stop
User=nexus
Restart=on-abort
 
[Install]
WantedBy=multi-user.target
EOT
 
echo "Configuring Nexus..."
sudo sh -c "echo 'run_as_user=\"nexus\"' > /opt/nexus/$NEXUS_DIR/bin/nexus.rc"
 
echo "Starting Nexus service..."
sudo systemctl daemon-reload
sudo systemctl enable nexus
sudo systemctl start nexus
 
echo "Waiting for Nexus to start (this may take a few minutes)..."
sleep 30
 
if systemctl is-active --quiet nexus; then
    IP_ADDRESS=$(hostname -I | awk '{print $2}')
 
    echo "=================================================="
    echo "Nexus installation completed successfully!"
    echo "Nexus is now running on: http://$IP_ADDRESS:8081"
    echo "=================================================="
    echo "Default credentials:"
    echo "Username: admin"
    echo "Password: Check /opt/nexus/sonatype-work/nexus3/admin.password"
    echo "=================================================="
else
    echo "Nexus service failed to start. Please check logs with: journalctl -u nexus.service -b"
    exit 1
fi
 
sudo rm -rf /tmp/nexus/

Nexus needs more CPU, memory and disk than a small VM usually has; on a small VM it fails to start. Check the Nexus system requirements before sizing the VM.

Open http://<server-ip>:8081 and click the sign-in icon at the top right:

Nexus Repository Community Edition browse page listing default maven and nuget repositories

The first admin password is in a file on the server:

Nexus sign-in dialog saying the admin password is in /opt/nexus/sonatype-work/nexus3/admin.password

terminal
$ sudo cat /opt/nexus/sonatype-work/nexus3/admin.password

The setup wizard then asks for a new password and whether to allow anonymous access. Disable it so that every user and build tool must authenticate:

Nexus setup wizard step Configure Anonymous Access with Disable anonymous access selected

Create a Maven hosted repository

Nexus comes with some repositories. A proxy repository caches a remote one (like Maven Central), a hosted repository stores your own files, and a group combines several behind one URL. Build outputs belong in a hosted repository.

Go to Settings (gear icon) → Repository → Repositories → Create repository:

Nexus Repositories settings page with Create repository button and the default proxy, group and hosted repositories

Choose the recipe maven2 (hosted):

Nexus Select Recipe list with maven2 (hosted) highlighted

Give it a name and create it. The lab repository is Java-app, with version policy Release:

Nexus repository Java-app: maven2 hosted, URL http://192.168.56.5:8081/repository/Java-app/, release version policy

Upload the WAR from Jenkins

Install the Nexus Artifact Uploader plugin:

Jenkins plugin search showing Nexus Artifact Uploader 2.14 selected for install

Add the Nexus login as a Username with password credential with ID nexus-cred:

Jenkins New credentials form: Username with password, username admin, ID nexus-cred, description Nexus credentials

Then add the upload stage after the build:

GRVJenkinsfile (excerpt)
stage("UploadArtifact") {
    steps {
        nexusArtifactUploader(
            nexusVersion: 'nexus3',
            protocol: 'http',
            nexusUrl: '192.168.56.5:8081',
            groupId: 'QA',
            version: "${env.BUILD_ID}-${env.BUILD_TIMESTAMP}",
            repository: 'Java-app',
            credentialsId: 'nexus-cred',
            artifacts: [
                [artifactId: 'java-tomcat-sample',
                 classifier: '',
                 file: 'target/java-tomcat-maven-example.war',
                 type: 'war']
            ]
        )
    }
}
FieldValueMeaning
nexusVersionnexus3Nexus 3 API
protocolhttpThe lab Nexus has no TLS
nexusUrl192.168.56.5:8081Host and port only, no http:// and no trailing slash
groupIdQATop folder in the repository
versionbuild ID + build timestampA new version for every build. A release repository normally refuses to overwrite an existing version.
repositoryJava-appThe hosted repository created above
credentialsIdnexus-credThe Jenkins credential
artifactIdjava-tomcat-sampleSame as in pom.xml
filetarget/java-tomcat-maven-example.warPath to the built WAR

The full pipeline now runs checkout, build, unit tests, Checkstyle, Sonar analysis and the upload:

Jenkins stage view: Checkout SCM, Build, UNIT TEST, Checkstyle Analysis, Sonar Analysis and UploadArtifact all passing

The WAR is in Nexus, together with .md5 and .sha1 checksum files that let a download be checked for damage:

Nexus browse of Java-app showing QA/java-tomcat-sample/9-null with the WAR and its md5 and sha1 files

The version is 9-null: 9 is the build number, but BUILD_TIMESTAMP was empty because the plugin that sets it was not installed. The console log of the next build shows the same thing:

Jenkins console log uploading java-tomcat-sample-10-null.war to the Java-app repository

The fix is the Build Timestamp plugin, which adds the BUILD_TIMESTAMP variable:

Jenkins plugin search showing the Build Timestamp plugin, which adds BUILD_TIMESTAMP to Jenkins variables

After installing it, new uploads carry the timestamp:

Nexus browse of Java-app with version 11-2025-09-20 17:09:42 UTC next to the older 9-null and 10-null versions

Instead of sharing the admin account, create users with only the rights they need in Settings → Security → Users → Create local user:

Nexus Security Users page with Create local user button and the admin and anonymous users

Troubleshooting

ProblemCause and fix
SonarQube stops shortly after startingUsually the Elasticsearch limits. Check vm.max_map_count with sysctl vm.max_map_count and read the files in /opt/sonarqube/logs/.
Coverage shows "Not computed"SonarQube needs the JaCoCo XML report. Generate it in the Maven build (JaCoCo report goal) and point sonar.coverage.jacoco.xmlReportPaths at it.
Gate passes although the code has issuesThe gate checks new code only. Issues in old code show on the Overall Code tab.
Nexus version is 9-nullBUILD_TIMESTAMP is not set. Install the Build Timestamp plugin.
Upload URL contains 8081//repositoryCaused by nexusUrl: '192.168.56.5:8081/'. Remove the trailing slash.
Version contains spaces and colons (11-2025-09-20 17:09:42 UTC)That is the default timestamp format. A pattern without spaces, set in the Build Timestamp section of Manage Jenkins → System, gives cleaner file names.
Nexus does not startThe VM is too small, or a path in the service file is wrong. Check journalctl -u nexus.service -b.

Key takeaways

  • SonarQube has a server (dashboard and gates) and a scanner (runs in the build). Jenkins installs the scanner as a tool and finds the server by name.
  • Store the SonarQube token as a Secret text credential; SonarQube shows it only once.
  • Quality gate conditions apply to new code. Add waitForQualityGate if a failed gate should stop the pipeline.
  • Nexus hosted repositories store build outputs; give every build a unique version.
  • nexusUrl is host and port only, and BUILD_TIMESTAMP needs the Build Timestamp plugin.