Introduction
SonarQube is a static analysis platform that inspects source code for bugs, vulnerabilities, and code smells on every build, then rolls the result into a single pass/fail decision called a quality gate. The value isn't the dashboard — it's wiring that gate directly into CI so a pull request that drops coverage, introduces a SQL injection pattern, or duplicates a large block of code simply cannot merge, without a human having to notice it in review.
This guide covers SonarQube the way it's actually run in production: a server backed by PostgreSQL, analysis triggered from a Jenkins pipeline via SonarScanner, and a quality gate that blocks the build through waitForQualityGate() rather than a gate that just sits unused in a dashboard nobody opens.
Who this is for
- DevOps/platform engineers adding an automated quality bar to an existing CI pipeline.
- Teams tired of code review catching style and security issues a machine should catch first.
- Anyone standing up SonarQube for the first time and wanting the production config, not just a quick-start.
Prerequisites
Have the following ready before standing up SonarQube:
- A Linux host (VM or container host) with at least 2 vCPU / 4GB RAM for a small team; more for larger codebases.
- Docker and Docker Compose, or a JVM 17 runtime if installing SonarQube manually.
- PostgreSQL 13+ (bundled via Docker Compose, or an external managed instance for production).
- Root/sudo access to raise the vm.max_map_count kernel setting Elasticsearch requires.
- A CI system (Jenkins used throughout this guide) with a pipeline already building and testing the project.
- SonarScanner CLI, or the Maven/npm scanner plugin matching your project's build tool.
Elasticsearch under the hood
SonarQube embeds Elasticsearch to index analysis results and power search. It inherits Elasticsearch's memory-mapping requirements — thevm.max_map_countcheck in the Installation section isn't optional, the server refuses to start without it.Theory
What static analysis actually finds
SonarQube parses source into an abstract syntax tree per language and runs a rule engine against it — no code execution required. Findings fall into three categories:
| Category | Meaning | Example |
|---|---|---|
| Bug | Code that is demonstrably wrong or will misbehave at runtime | Null pointer dereference on a path the analyzer can prove is reachable |
| Vulnerability | Code that is exploitable from a security standpoint | String-concatenated SQL query built from unsanitized user input |
| Code Smell | Maintainability issue — not wrong, but expensive to live with | Cognitive complexity too high, duplicated block, dead code |
Technical debt and the debt ratio
Every code smell carries an estimated remediation cost in minutes. SonarQube sums these into a technical debt figure and expresses it as a debt ratio— remediation cost divided against the cost of rewriting the codebase from scratch. A low ratio (SonarQube's default target is under 5%) is what the Sonar way quality profile enforces as a maintainability rating of A.
Coverage isn't computed by SonarQube
SonarQube does not execute your tests. Coverage tools (JaCoCo, Istanbul/nyc, coverage.py, etc.) generate a report during the build, and the scanner simply imports and attributes it to lines and new code. If the coverage report path is wrong or missing, SonarQube will silently show 0% coverage rather than error — a very common false alarm.
New Code vs. Overall Code
Modern quality gates evaluate almost entirely against New Code — the diff introduced since a defined baseline (typically the previous version or the last 30 days) — rather than the entire, possibly legacy-riddled codebase. This is what makes adopting SonarQube on a large existing codebase practical: you gate only what changes going forward.
Architecture
A production SonarQube setup has four moving parts: the CI pipeline that triggers analysis, the scanner that walks the source tree and uploads results, the SonarQube server that stores and evaluates them, and a webhook that reports the gate result back to CI so it can block or allow the build.
CI triggers SonarScanner, which reports to the SonarQube server; the quality gate result flows back to CI via webhook
The scanner does not decide pass/fail
The scanner uploads raw analysis data and exits successfully as soon as the upload succeeds — the quality gate evaluation happens asynchronously on the server afterward. This is why CI must explicitly wait for the gate result (see Real World Example) rather than trusting the scanner's own exit code.Installation
Raise the Elasticsearch memory-map limit on the host first — this applies regardless of which installation method you choose:
# Required on the Docker host / VM, not inside the container
sudo sysctl -w vm.max_map_count=262144
# Persist across reboots
echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.confThe fastest reliable path to a working server — SonarQube and PostgreSQL as two services sharing a network, with named volumes so data survives container recreation.
version: "3.8"
services:
sonarqube:
image: sonarqube:10-community
container_name: sonarqube
depends_on:
- db
ports:
- "9000:9000"
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonar
SONAR_JDBC_USERNAME: sonar
SONAR_JDBC_PASSWORD: ${SONAR_DB_PASSWORD}
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
ulimits:
nofile:
soft: 65536
hard: 65536
db:
image: postgres:15
container_name: sonarqube_db
environment:
POSTGRES_USER: sonar
POSTGRES_PASSWORD: ${SONAR_DB_PASSWORD}
POSTGRES_DB: sonar
volumes:
- postgresql_data:/var/lib/postgresql/data
volumes:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
postgresql_data:export SONAR_DB_PASSWORD=$(openssl rand -base64 24)
docker compose up -d
docker compose logs -f sonarqube # watch for "SonarQube is operational"First login
Default credentials areadmin / admin on first boot — the UI forces a password change immediately. Rotate this further into a personal account plus scoped tokens as covered in Security Hardening; nothing should authenticate as admin on an ongoing basis.Configuration
Quality Profiles
A quality profileis the set of rules actually enforced per language (e.g. "Sonar way" for Java, JavaScript, Python). Profiles are assigned per-project, so different teams can enforce stricter or looser rule sets without affecting each other. Clone the built-in "Sonar way" profile before customizing it — the built-in profile is managed by SonarSource and your edits would be lost on upgrade.
Quality Gates
A quality gate is a set of conditions evaluated against the New Code of an analysis; if any condition fails, the whole gate fails. The default Sonar way gate is a reasonable starting point:
| Condition (on New Code) | Default threshold |
|---|---|
| Coverage | ≥ 80% |
| Duplicated Lines | ≤ 3% |
| Maintainability Rating | A |
| Reliability Rating | A |
| Security Rating | A |
| Security Hotspots Reviewed | 100% |
Custom gates are created under Quality Gates → Create and can add conditions like a hard cap on new blocker/critical issues, or tighten coverage per project. Assign a project to a custom gate under Project Settings → Quality Gate.
Gate failure is the whole point
A red quality gate is meant to block— that's the enforcement mechanism. If a gate never fails in practice, either the thresholds are too loose or nobody has wiredwaitForQualityGate() into CI yet (see Real World Example).sonar-project.properties
For projects not using the Maven or npm scanner plugins, a sonar-project.properties file at the repo root configures the CLI scanner:
sonar.projectKey=payments-service
sonar.projectName=Payments Service
sonar.projectVersion=1.0
# Source and test locations
sonar.sources=src
sonar.tests=test
sonar.exclusions=**/node_modules/**,**/*.spec.js,**/vendor/**
# Coverage report import (language-specific)
sonar.javascript.lcov.reportPaths=coverage/lcov.info
# Server location (usually overridden via -D flags in CI instead)
sonar.host.url=http://sonarqube.internal:9000
# Encoding
sonar.sourceEncoding=UTF-8Never commit the auth token
Do not addsonar.login or sonar.token to sonar-project.properties — pass the token as an environment variable or-D flag from CI (see Commands) so it never lands in version control.Commands
SonarScanner CLI — generic / any language
# Analyze using sonar-project.properties in the current directory
sonar-scanner \
-Dsonar.host.url=http://sonarqube.internal:9000 \
-Dsonar.token=$SONAR_TOKEN
# Override the project key/version at analysis time
sonar-scanner \
-Dsonar.projectKey=payments-service \
-Dsonar.projectVersion=$BUILD_NUMBER \
-Dsonar.host.url=http://sonarqube.internal:9000 \
-Dsonar.token=$SONAR_TOKENMaven projects — sonar-scanner-maven
mvn clean verify sonar:sonar \
-Dsonar.host.url=http://sonarqube.internal:9000 \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xmlNo plugin declaration is needed beyond adding the SonarScanner Maven plugin once in the parent POM's pluginManagement section.
npm / Node projects — sonar-scanner-npm
# Installed as a devDependency: npm install --save-dev sonarqube-scanner
npx sonar-scanner \
-Dsonar.projectKey=web-frontend \
-Dsonar.sources=src \
-Dsonar.host.url=http://sonarqube.internal:9000 \
-Dsonar.token=$SONAR_TOKEN \
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.infoPython projects
# pytest-cov generates coverage.xml consumed directly by the scanner
pytest --cov=app --cov-report=xml
sonar-scanner \
-Dsonar.projectKey=data-pipeline \
-Dsonar.sources=app \
-Dsonar.python.coverage.reportPaths=coverage.xml \
-Dsonar.host.url=http://sonarqube.internal:9000 \
-Dsonar.token=$SONAR_TOKENExamples
A typical scan of a mid-size service, run manually to validate configuration before wiring it into CI:
$ sonar-scanner -Dsonar.host.url=http://sonarqube.internal:9000 -Dsonar.token=$SONAR_TOKEN
INFO: Scanner configuration file: /opt/sonar-scanner/conf/sonar-scanner.properties
INFO: Project root configuration file: /repo/sonar-project.properties
INFO: SonarScanner 5.0.1.3006
INFO: Analyzing on SonarQube server 10.5.1
INFO: Load global settings
INFO: Base dir: /repo
INFO: Working dir: /repo/.scannerwork
INFO: 142 files indexed
INFO: Quality profile for js: Sonar way
INFO: Sensor JavaScript analysis [javascript]
INFO: 8 new issues, 0 blocker, 1 critical, 3 major, 4 minor
INFO: Coverage data imported: 84.2% of new code
INFO: Analysis report generated in 1834ms
INFO: ANALYSIS SUCCESSFUL, you can find the results at: http://sonarqube.internal:9000/dashboard?id=web-frontend
INFO: Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report
INFO: More about the report processing at http://sonarqube.internal:9000/api/ce/task?id=AY3x9k2pQzThe server processes the report asynchronously; polling that task URL is exactly what waitForQualityGate() does under the hood in CI.
Report submitted
Scanner finishes uploading and printsANALYSIS SUCCESSFUL— this only means the upload worked, not that the gate passed.Background computation
The server's Compute Engine processes the report (usually 5-30 seconds) and evaluates it against the assigned quality gate.Gate result: FAILED
Dashboard shows 1 critical issue and coverage 84.2% vs. required 80% — coverage passes, but the Reliability Rating condition fails because of the critical bug, so the overall gate is red.Webhook fires
SonarQube POSTs the gate result to the configured webhook URL; Jenkins'waitForQualityGate()receives it and fails the pipeline stage.
Screenshots
Project dashboard showing Bugs, Vulnerabilities, Code Smells, Coverage, and Duplications for the latest analysis
Screenshot placeholder
Quality Gate status panel showing a failed 'Reliability Rating on New Code' condition blocking the merge
Screenshot placeholder
Real World Example
Scenario: every pull request into main should run tests, run a SonarQube analysis, and fail the build outright if the quality gate is red — no manual dashboard-checking required.
pipeline {
agent any
environment {
SONAR_TOKEN = credentials('sonarqube-analysis-token')
}
stages {
stage('Build & Test') {
steps {
sh 'npm ci'
sh 'npm test -- --coverage'
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('sonarqube-server') {
sh '''
sonar-scanner \
-Dsonar.projectKey=web-frontend \
-Dsonar.sources=src \
-Dsonar.javascript.lcov.reportPaths=coverage/lcov.info \
-Dsonar.token=$SONAR_TOKEN
'''
}
}
}
stage('Quality Gate') {
steps {
// Blocks the pipeline here until SonarQube's webhook reports
// back; times out rather than hanging forever if the webhook
// is misconfigured.
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
stage('Deploy') {
when { branch 'main' }
steps {
sh './deploy.sh'
}
}
}
}withSonarQubeEnv comes from the SonarQube Scanner Jenkins plugin and injects the server URL and a task ID that waitForQualityGate later correlates with the webhook callback. Configure the matching webhook once, server-side, pointing back at Jenkins:
Name: jenkins-quality-gate
URL: http://jenkins.internal:8080/sonarqube-webhook/PR opened, pipeline triggers
Jenkins checks out the PR branch and runs the Build & Test stage.Scanner uploads analysis
The SonarQube Analysis stage runssonar-scanner; Jenkins records the associated Compute Engine task ID and moves on rather than blocking here.Pipeline blocks on the gate
waitForQualityGate abortPipeline: truepauses the pipeline (without occupying an executor) until SonarQube's webhook fires with a pass/fail result.Gate fails, PR blocked
A red gate makes the stage fail, which fails the whole pipeline. Combined with a branch protection rule requiring this Jenkins check to pass, the PR is mechanically unmergeable until the issues are fixed.
Common Issues
Scanner can't reach the SonarQube server
ERROR: SonarQube server [...] can not be reached almost always means a network/firewall issue between the CI agent and the server, or the wrong sonar.host.url (e.g. localhostused inside a container that can't see the host). Confirm with a plain curl -I $SONAR_HOST_URL/api/system/status from the CI agent itself before touching scanner config.Quality gate stuck 'pending' / waitForQualityGate never returns
This is a missing or misconfigured webhook, not a slow server. If SonarQube has no webhook pointed at Jenkins, the gate is computed correctly but Jenkins never hears about it and hangs until the surroundingtimeout() block kills it. Check Administration → Configuration → Webhooks for a delivery log with a non-200 response.Server fails to start: 'max virtual memory areas vm.max_map_count is too low'
Elasticsearch, embedded inside SonarQube, refuses to boot below the required memory-map limit. Runsudo sysctl -w vm.max_map_count=262144 on the Docker host (not inside the container) and persist it in /etc/sysctl.conf.Duplicate projects appearing for the same repo
Caused by an inconsistentsonar.projectKey across branches or CI jobs — often a default auto-generated key on first run followed by an explicit key added later. Fix the key in sonar-project.properties (or the scanner invocation) to be identical everywhere, then delete the stray duplicate project from the UI.Analysis fails or shows wrong 'New Code' on a shallow git clone
SonarScanner reads git blame history to attribute lines to authors and to compute the New Code period accurately. A CI checkout with--depth 1 hides that history, producing warnings like "Shallow clone detected" and inaccurate New Code stats. Use a full clone, or at minimum git fetch --unshallow before running the scanner.Best Practices
- Gate on New Code, not the whole codebase — it's the only way to adopt SonarQube on a large legacy repo without an impossible backlog blocking every PR.
- Keep the default "Sonar way" gate unless you have a specific, documented reason to loosen it — tightening is safer than loosening.
- Run analysis on every PR, not just on merge to main — catching issues before merge is the entire point of blocking the gate.
- Version-pin the SonarScanner CLI and Jenkins plugin in CI so a scanner upgrade doesn't silently change gate behavior mid-sprint.
- Always fetch full git history (no shallow clones) in CI so blame-based New Code detection and issue assignment are accurate.
- Review Security Hotspots on a schedule — they require a human judgment call and don't fail the gate by default the way vulnerabilities do.
Security Hardening
- Generate a scoped analysis token per project or CI job (My Account → Security) and rotate it periodically — never authenticate CI as the
adminaccount or a personal password. - Restrict project permissions so only the CI service account has "Execute Analysis" rights, and only specific teams have "Administer" on their own projects.
- Run SonarQube behind a reverse proxy (nginx/Apache) terminating TLS — the built-in web server should never be exposed directly to the internet on plain HTTP.
- Keep the full "Sonar way" security rule set enabled, not just bug/smell rules — vulnerability and security hotspot detection is what actually prevents shippable CVE-class issues.
- Disable the default
admin/adminaccount's password entirely after creating named administrator accounts with SSO/LDAP where available. - Store
SONAR_TOKENas a CI credential/secret (e.g. Jenkins Credentials), never insonar-project.propertiesor a Jenkinsfile literal.
Interview Questions
Cheat Sheet
# --- Server (Docker) ---
docker compose up -d
docker compose logs -f sonarqube
# --- Kernel prereq (Elasticsearch) ---
sudo sysctl -w vm.max_map_count=262144
# --- sonar-project.properties (key fields) ---
sonar.projectKey=<unique-key>
sonar.sources=src
sonar.tests=test
sonar.exclusions=**/node_modules/**
sonar.sourceEncoding=UTF-8
sonar.<lang>.lcov.reportPaths=coverage/lcov.info
# --- CLI scanner ---
sonar-scanner -Dsonar.host.url=<url> -Dsonar.token=$SONAR_TOKEN
# --- Maven ---
mvn verify sonar:sonar -Dsonar.token=$SONAR_TOKEN
# --- npm ---
npx sonar-scanner -Dsonar.token=$SONAR_TOKEN
# --- Jenkinsfile essentials ---
withSonarQubeEnv('sonarqube-server') { sh 'sonar-scanner ...' }
timeout(time: 5, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
# --- Webhook target (configure server-side) ---
http://<jenkins-host>:8080/sonarqube-webhook/Summary
SonarQube earns its place in a pipeline the moment its quality gate can actually stop a bad merge — everything before that (dashboards, ratings, profiles) is just setup. The durable pattern is: scan on every PR with the right scanner for your build tool, gate on New Code so legacy debt doesn't block progress, configure the webhook so CI hears back, and call waitForQualityGate() so a red gate fails the build instead of quietly sitting in a dashboard. Get that loop closed once, and the quality bar enforces itself from then on.
Resources
- Official SonarQube documentation —
docs.sonarsource.com - SonarSource rules explorer (per-language rule catalog) —
rules.sonarsource.com - SonarScanner CLI reference —
docs.sonarsource.com - Jenkins SonarQube Scanner plugin —
plugins.jenkins.io - PostgreSQL documentation —
postgresql.org/docs