Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Static Code Analysis in CI with SonarQube

By Kokil Thapa | Last reviewed: September 2026

Pull requests that pass tests can still ship SQL injection, duplicated logic, and unmaintainable debt. Static Code Analysis in CI with SonarQube scans every commit before merge and enforces rules your team agrees on. I run this pattern on production Laravel apps deployed through GitLab CI pipelines for Laravel. The scanner finds issues PHPUnit never touches. This guide walks through server setup, scanner config, quality gates, and a working pipeline you can copy today.

What is Static Code Analysis in CI with SonarQube and why use it?

SonarQube reads source code without executing it. It flags bugs, security hotspots, code smells, duplication, and test coverage gaps. Wired into CI, it becomes a gate—not a report nobody reads.

On client projects I maintain with Deployer 7 and GitLab CI, SonarQube sits between unit tests and deploy. A green test suite plus a red quality gate still blocks the merge. That single rule has prevented several regressions that would have reached staging.

Static analysis complements—not replaces—tools like PHPStan. Pair SonarQube with PHPStan level 9 analysis for type safety SonarQube does not fully cover. SonarQube adds security rules, duplication tracking, and a dashboard non-developers can read.

SonarQube in the CI PipelineGit PushFeature branchCI BuildTests + coverageSonarScanUpload reportQuality GatePass or failGate OutcomesPASSMerge allowedDeploy continuesFAILMerge blockedFix before merge
Static Code Analysis in CI with SonarQube: every push runs tests, uploads a scan, and evaluates the quality gate before merge.

The return on investment is straightforward. Fixing a SQL injection in a pull request costs minutes. Fixing it in production costs hours and client trust. For teams offering testing and optimization services, SonarQube gives measurable quality metrics before handoff.

How do you install and configure SonarQube for CI pipelines?

SonarQube runs as a long-lived server. CI jobs only run the lightweight SonarScanner CLI and upload results. You do not need SonarQube on every developer laptop.

Server deployment options

For small teams, run SonarQube Community Edition on a dedicated Ubuntu 24.04 VM with at least 4 GB RAM. Docker Compose works well for staging. Production setups should use a managed PostgreSQL 18 database—not the embedded H2 database SonarQube ships for trials.

A minimal Docker Compose stack looks like this:

services:
  sonarqube:
    image: sonarqube:community
    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_logs:/opt/sonarqube/logs
      - sonarqube_extensions:/opt/sonarqube/extensions

  db:
    image: postgres:18
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: ${SONAR_DB_PASSWORD}
      POSTGRES_DB: sonar

After first boot, log in at port 9000 with the default admin credentials. Change the password immediately. Generate a project token under My Account → Security. Store it in your CI secret manager—never commit it to Git.

Project and branch setup

Create one SonarQube project per repository. Enable branch analysis if you use Developer Edition or above. Community Edition supports main-branch analysis only; feature branches still scan, but gating on branch conditions requires a paid tier or a single long-lived integration branch strategy.

For Laravel 12 or 13 apps on PHP 8.3+, install the PHP plugin from Administration → Marketplace if it is not bundled. Confirm PHP, JavaScript, and CSS analyzers are active for full-stack projects like Adventure Third Pole Trek.

SonarQube ArchitectureGitLab CI JobSonarScanner CLISonarQube ServerRules + gatesPostgreSQL 18Issues historyAnalysis ArtifactsCoverage XMLSource filesTest reports
SonarScanner uploads source, coverage, and test data to the SonarQube server, which persists history in PostgreSQL.

How do you configure sonar-project.properties for Laravel and PHP?

Place a sonar-project.properties file in your repository root. This file tells SonarScanner what to scan and where coverage lives.

sonar.projectKey=my-laravel-app
sonar.projectName=My Laravel App
sonar.projectVersion=${CI_COMMIT_SHORT_SHA}

sonar.sources=app,routes,resources/js
sonar.tests=tests
sonar.sourceEncoding=UTF-8

sonar.exclusions=/vendor/,/storage/,/bootstrap/cache/,\
/node_modules/,/public/build/

sonar.php.coverage.reportPaths=storage/coverage/clover.xml
sonar.php.tests.reportPath=storage/coverage/junit.xml

sonar.host.url=${SONAR_HOST_URL}
sonar.token=${SONAR_TOKEN}

Generate Clover coverage during PHPUnit or Pest runs. Without coverage data, SonarQube cannot enforce coverage gates. See code coverage gates in CI for the PHPUnit flags that produce clover.xml reliably.

Exclusions that matter for Laravel

Always exclude vendor/, compiled views, and Vite build output. Including them inflates line counts and produces false duplication alerts. For Blade templates, add resources/views to sources only if you want HTML duplication checks—many teams exclude views and rely on PHP analysis in controllers and Livewire components instead.

Validate your config locally before pushing:

docker run --rm \
  -e SONAR_HOST_URL=https://sonar.example.com \
  -e SONAR_TOKEN=squ_xxxx \
  -v "$(pwd):/usr/src" \
  sonarsource/sonar-scanner-cli

Official reference: the SonarScanner documentation covers CLI flags and environment variable overrides.

How do you wire SonarQube into GitLab CI for Laravel projects?

GitLab CI is my default for Laravel deployments. The scan job should run after tests produce coverage artifacts. Use needs: to avoid waiting for unrelated stages.

stages:
  - test
  - quality
  - deploy

unit-tests:
  stage: test
  image: php:8.3-cli
  script:
    - composer install --no-interaction --prefer-dist
    - php artisan test --coverage-clover=storage/coverage/clover.xml \
        --log-junit=storage/coverage/junit.xml
  artifacts:
    paths:
      - storage/coverage/
    expire_in: 1 day

sonarqube:
  stage: quality
  image: sonarsource/sonar-scanner-cli:latest
  needs: [unit-tests]
  variables:
    SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar"
    GIT_DEPTH: "0"
  script:
    - sonar-scanner
  allow_failure: false

Set GIT_DEPTH: "0" for accurate blame and new-code metrics. Shallow clones break leak-period calculations. Store SONAR_TOKEN and SONAR_HOST_URL as masked CI variables.

For pull-request decoration, configure a webhook from SonarQube to GitLab. Developers then see inline comments on merge requests—a pattern I use alongside build verification and quality gates on sister legal-tech sites sharing the same Deployer pipeline.

GitHub Actions alternative

Teams on GitHub can use the official SonarCloud or self-hosted action. The scanner invocation stays identical; only the YAML wrapper changes. Compare runner models in our GitHub Actions vs GitLab CI guide if you are choosing a platform.

Quality Gate Decision TreeScan CompleteNew code conditions met?NoYesGATE FAILBlock merge in CIGATE PASSAllow mergeConditions: zero new bugs, zero new vulns, coverage on new code ≥ 80%, duplication ≤ 3%
SonarQube evaluates new-code conditions first; any breach fails the quality gate and blocks the CI pipeline.

What quality gate thresholds should you set for production PHP apps?

Start with SonarQube's built-in "Sonar way" profile, then tighten gradually. Enforcing 90% coverage on day one will freeze your team. A phased approach works better on real client timelines.

I recommend this rollout sequence:

  1. Week 1: Run scans in report-only mode with allow_failure: true. Fix critical and blocker issues only.
  2. Week 2–3: Enable the gate on new code—zero new bugs, zero new vulnerabilities, security hotspots reviewed.
  3. Month 2: Add coverage on new code at 70%, then raise to 80% once the team adapts.
  4. Ongoing: Track technical debt ratio and duplication; schedule cleanup sprints when debt exceeds five days.

Detailed gate tuning belongs in your SonarQube quality and security gates reference. Align thresholds with your test strategy in Laravel Pest CI/CD testing.

ToolPrimary strengthCI gate supportBest paired with SonarQube?
SonarQubeSecurity, duplication, coverage dashboardNative quality gate
PHPStanStatic typing, generics, level 9 rulesExit code in CI scriptYes — complementary
PHPUnit / PestRuntime behaviour verificationTest failure blocks buildYes — feeds coverage
GitleaksSecret detection in Git historyExit code on leak foundYes — see secrets scanning with Gitleaks
ESLint / PintStyle and lint for JS/PHPFormatter exit codePartial overlap on smells

SonarQube is not a replacement for secret scanning or dependency audit tools. Run composer audit in a separate job. Combine layers rather than expecting one scanner to catch everything.

How do you troubleshoot common SonarQube CI failures?

Most pipeline failures fall into a handful of categories. Knowing which saves hours of log spelunking.

Coverage shows 0% despite passing tests

The clover.xml path in sonar-project.properties must match the artifact path exactly. Confirm the test job uploads coverage before the scan job runs. A missing needs: dependency causes the scanner to run against an empty directory.

Quality gate fails on legacy code you did not touch

Switch conditions to "on new code" only. SonarQube calculates a leak period from your previous release or a set number of days. Legacy debt stays visible on the dashboard but does not block merges.

Scanner timeout or out-of-memory on large repos

Increase the CI job memory limit. Exclude generated assets aggressively. Use CI build caching for Composer and npm so the test stage finishes faster and the scan stage starts sooner.

Authentication errors

Regenerate the project token if it was rotated. Confirm SONAR_HOST_URL has no trailing slash mismatch. Self-signed TLS certificates require importing the CA into the scanner image or using SONAR_SCANNER_OPTS for trust configuration—prefer proper Let's Encrypt certs on the server instead.

For infrastructure issues on the SonarQube VM itself—disk space, PostgreSQL connection limits, Java heap—basic Linux server administration skills apply. SonarQube logs live under logs/sonar.log inside the container or install directory.

Manual Review vs SonarQube CIBefore: Manual OnlyInconsistent review depthSecurity gaps missedNo coverage trackingDebt grows silentlyLate production fixesCost: hours per releaseAfter: SonarQube CIEvery commit scannedSecurity rules enforcedCoverage on new codeDebt visible in dashboardIssues caught pre-mergeCost: ~Rs 3,000/mo hostUpgrade
Static Code Analysis in CI with SonarQube replaces inconsistent manual review with automated, measurable gates on every push.

When debugging JSON config or webhook payloads during setup, a local JSON formatter saves time validating API responses from SonarQube's Web API.

Key Takeaways

  • Run SonarQube as a persistent server; CI jobs only execute SonarScanner and upload results.
  • Place sonar-project.properties in the repo root with correct Laravel exclusions and Clover coverage paths.
  • Wire the scan job after tests with needs: and set GIT_DEPTH: "0" for accurate new-code metrics.
  • Roll out quality gates gradually—new-code conditions first, coverage thresholds second.
  • Pair SonarQube with PHPStan, secret scanning, and composer audit for layered defence.
  • Store SONAR_TOKEN in CI secrets and follow CI/CD secrets management best practices.

People Also Ask

Is SonarQube free for CI pipelines?

SonarQube Community Edition is free and supports main-branch analysis with quality gates. Branch and pull-request decoration on multiple long-lived branches requires Developer Edition or SonarCloud. For a single integration branch workflow, Community Edition is enough for most small Laravel teams.

Does SonarQube replace PHPUnit or Pest?

No. SonarQube performs static analysis—it reads code without running it. PHPUnit and Pest verify runtime behaviour. SonarQube consumes coverage reports that PHPUnit or Pest generate, then enforces coverage thresholds on new code through the quality gate.

How long does a SonarQube scan take in CI?

A typical Laravel app with 50,000 lines of PHP scans in two to five minutes after tests complete. First scans take longer because SonarQube builds its issue index. Exclude vendor and build directories to keep scan times predictable on shared GitLab runners.

Can SonarQube analyze WordPress or WooCommerce plugins?

Yes. Point sonar.sources at your plugin directory and exclude wp-content/uploads and vendor paths. WooCommerce 11.1 custom themes benefit from the same gate pattern. For WordPress-specific workflows, see our WordPress development services page for project context.

Ship cleaner code on every merge

Static Code Analysis in CI with SonarQube turns code quality from a subjective review comment into a measurable gate. Install the server once, add a scanner job to your pipeline, and tighten thresholds as the team matures. The setup pays for itself the first time it blocks a vulnerability before production.

Need help wiring SonarQube into an existing Laravel or legal-tech codebase? Explore our custom software development work—including platforms like Mijar Law Associates—or contact us to audit your CI pipeline and quality gates.

Frequently Asked Questions

It runs SonarScanner on each build, uploads results to SonarQube, evaluates a quality gate, and fails the pipeline when configured thresholds for bugs, vulnerabilities, coverage, or duplication are breached.

Community Edition is free with main-branch analysis and quality gates. Branch analysis and pull-request decoration need Developer Edition or SonarCloud.

No. SonarQube reads code statically; PHPUnit and Pest verify runtime behaviour and generate coverage reports SonarQube consumes for gate enforcement.

They solve different problems. PHPUnit and Pest catch runtime failures; PHPStan enforces static typing at level 9; SonarQube adds security rules, duplication tracking, coverage dashboards, and a quality gate non-developers can read. On Laravel apps I deploy through GitLab CI and Deployer 7, SonarQube sits between unit tests and deploy—a green test suite plus a red gate still blocks merge. Pair all three rather than expecting one scanner to catch SQL injection, type errors, and missing tests.

Run SonarQube as a long-lived server; CI jobs only execute the lightweight SonarScanner CLI and upload results. For small teams, deploy Community Edition on a dedicated Ubuntu 24.04 VM with at least 4 GB RAM, or use Docker Compose for staging. Production setups should use managed PostgreSQL 18, not the embedded H2 database. After first boot at port 9000, change default admin credentials, create one project per repository, generate a project token under My Account → Security, and store it in your CI secret manager—never commit it to Git.

Place the file in your repository root. Set sonar.projectKey, sonar.projectName, and sonar.projectVersion from your CI commit SHA. Point sonar.sources at app, routes, and resources/js; sonar.tests at tests. Exclude vendor/, storage/, bootstrap/cache/, node_modules/, and public/build/ to avoid false duplication and inflated line counts. Map sonar.php.coverage.reportPaths to storage/coverage/clover.xml and sonar.php.tests.reportPath to storage/coverage/junit.xml, generated during PHPUnit or Pest runs with coverage flags. Pass sonar.host.url and sonar.token via environment variables, not hard-coded values.

Add a quality stage after tests. The unit-tests job runs composer install and php artisan test with Clover and JUnit output saved as artifacts under storage/coverage/. The sonarqube job uses sonarsource/sonar-scanner-cli:latest, declares needs: [unit-tests], sets GIT_DEPTH: "0" for accurate blame and new-code metrics, and runs sonar-scanner with allow_failure: false once gates are active. Store SONAR_TOKEN and SONAR_HOST_URL as masked CI variables. For inline merge request comments, configure a webhook from SonarQube to GitLab. This is the pattern I use on production Laravel and legal-tech pipelines alongside Deployer 7 deployments.

Start with SonarQube's built-in Sonar way profile and tighten gradually—90% coverage on day one will freeze your team. Week 1: scan in report-only mode with allow_failure: true and fix critical or blocker issues only. Weeks 2–3: enable new-code conditions—zero new bugs, zero new vulnerabilities, security hotspots reviewed. Month 2: add 70% coverage on new code, then raise to 80% once the team adapts. Ongoing: track technical debt ratio and duplication; schedule cleanup when debt exceeds five days. Align thresholds with your Laravel Pest or PHPUnit CI testing strategy.

Community Edition is free and supports main-branch analysis with quality gates—enough for most small Laravel teams using a single integration branch workflow. Feature branches still scan on Community Edition, but gating on branch conditions and pull-request decoration across multiple long-lived branches requires Developer Edition or SonarCloud. If your team relies on merge-request inline comments and per-branch gate enforcement, budget for a paid tier or restructure around one integration branch. For sister legal-tech sites on shared GitLab CI pipelines, I evaluate this trade-off early because workflow shape drives edition choice.

A typical Laravel application with roughly 50,000 lines of PHP scans in two to five minutes after tests complete. First scans take longer because SonarQube builds its issue index. Keep times predictable by excluding vendor and build directories in sonar-project.properties, caching Composer and npm dependencies in the test stage, and ensuring the scan job waits only for the test job via needs: rather than unrelated stages. Large repositories hitting timeout or memory limits should increase CI job memory and exclude generated assets aggressively.

The clover.xml path in sonar-project.properties must match the artifact path exactly—typically storage/coverage/clover.xml from php artisan test --coverage-clover. Confirm the test job uploads coverage artifacts before the scan job runs. A missing needs: dependency causes the scanner to run against an empty directory with no Clover or JUnit files. Verify PHPUnit or Pest actually writes both clover.xml and junit.xml to storage/coverage/. Without coverage data uploaded, SonarQube cannot enforce coverage gates even when every test passes.

Switch gate conditions to on new code only. SonarQube calculates a leak period from your previous release or a configured number of days, so legacy debt remains visible on the dashboard but does not block merges. This phased rollout matches real client timelines: Week 1 runs scans in report-only mode, then Weeks 2–3 enforce zero new bugs and zero new vulnerabilities on new code only. Month 2 adds coverage thresholds on new code. Legacy issues get scheduled cleanup sprints when technical debt exceeds five days rather than blocking every pull request.

Generate a project token under My Account → Security in the SonarQube UI and store it as SONAR_TOKEN in your CI secret manager alongside SONAR_HOST_URL—both masked in GitLab CI variables. Never commit tokens to Git. Rotate tokens if authentication errors appear after a rotation. Confirm SONAR_HOST_URL has no trailing slash mismatch. For self-hosted servers, prefer proper Let's Encrypt certificates over importing CAs into the scanner image. Follow broader CI/CD secrets management practices: separate tokens per project, least privilege, and no plaintext in pipeline logs.

Yes. Point sonar.sources at your plugin or theme directory and exclude wp-content/uploads and vendor paths using sonar.exclusions, the same pattern as Laravel vendor exclusions. WooCommerce 11.1 custom themes benefit from the same quality gate workflow—scan after tests, enforce new-code conditions, and block merge on vulnerabilities or duplication thresholds. Install the PHP plugin from Administration → Marketplace if not bundled, and confirm PHP, JavaScript, and CSS analyzers are active for full-stack WordPress projects with Vite or custom JS assets.

Always exclude vendor/, compiled views, and Vite build output under public/build/. Including them inflates line counts and triggers false duplication alerts. Standard sonar.exclusions cover storage/, bootstrap/cache/, and node_modules/ as well. For Blade templates, add resources/views to sonar.sources only if you want HTML duplication checks—many teams exclude views and rely on PHP analysis in controllers and Livewire components instead. Validate exclusions locally with docker run sonarsource/sonar-scanner-cli before pushing, so your first CI scan reflects application code rather than generated artefacts.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: