想在代码合并前自动发现 Bug、漏洞、重复代码和低覆盖率,SonarQube 是最常见的自托管代码质量平台之一。但它并不是“随便启动一个容器”就能长期稳定运行:内置 Elasticsearch 对内核参数、内存和磁盘空间有明确要求,生产环境还需要独立 PostgreSQL、HTTPS、访问控制、备份和升级回滚方案。
本文基于 SonarQube Community Build 26.9.0.129388,在 Ubuntu VPS 上用 Docker Compose 部署 SonarQube、PostgreSQL 17 和 Caddy,并接入 Jenkins 与 GitHub Actions。最终目标不是只看到登录页,而是让一次真实提交触发扫描、返回 Quality Gate,并且数据库能够恢复。
SonarQube Community Build 按月发布。本文锁定
26.9.0.129388-community,不要在生产环境直接使用会自动漂移的latest或community标签。升级前应重新阅读官方发布说明并备份数据库。
SonarQube 会把静态分析结果集中到一个 Web 平台,适合以下场景:
- 团队希望在 Pull Request 或发布前检查代码质量;
- Jenkins、GitHub Actions、GitLab CI 等流水线需要统一 Quality Gate;
- 多个项目需要共享规则、质量配置和审计记录;
- 不想把私有代码分析结果全部交给第三方 SaaS;
- 需要对新代码的可靠性、安全性、可维护性、覆盖率和重复率设置门槛。
它不能替代单元测试、依赖漏洞管理、动态安全测试和人工 Code Review。更合理的组合是:测试负责验证行为,SonarQube 负责静态分析与质量门禁,Jenkins CI/CD 或 GitHub Actions 负责自动执行,Harbor 私有镜像仓库负责镜像存储和漏洞扫描。
| 方案 | 适合场景 | 主要优点 | 需要承担的工作 |
|---|---|---|---|
| Community Build | 个人、小团队、基础静态分析 | 免费、自托管、上手快 | 自己维护 VPS、数据库、升级和备份 |
| SonarQube Server 商业版 | 企业、多团队、复杂治理 | 更完整的分支、企业集成和安全能力 | 授权成本与更高资源需求 |
| SonarQube Cloud | 不想维护服务器 | 开通快、无需管理数据库 | 代码与分析元数据进入云服务,按产品规则计费 |
本文部署免费 Community Build。采购 VPS 前,先确认免费版的语言和 DevOps 集成功能满足实际需求;不要部署完成后才发现需要商业版本能力。
SonarQube 内部包含 Elasticsearch,资源需求明显高于普通博客或反向代理。官方给出的小规模实例起点是 2 核、4GB RAM 和 30GB 磁盘,且应至少保留 10% 空闲磁盘。把 PostgreSQL 也放在同一台机器时,建议再留出余量。
| 使用规模 | 建议配置 | 说明 |
|---|---|---|
| 个人测试、少量仓库 | 2 核 4GB、50GB NVMe、2GB Swap | 可以启动,但扫描和升级时余量有限 |
| 小团队生产使用 | 4 核 8GB、100GB NVMe | SonarQube 与 PostgreSQL 同机更稳妥 |
| 多团队、高频扫描 | 8 核 16GB+、200GB+ NVMe | 建议数据库独立部署,并持续监控容量 |
CPU 只是扫描速度的一部分。数据库延迟、NVMe 随机读写、代码规模、语言插件和并发流水线都会影响体验。如果还没决定套餐,可先参考VPS 配置选择指南。
本文使用下面的结构:
开发者 / CI Runner
│ HTTPS 443
▼
Caddy
│ Docker 内网 HTTP 9000
▼
SonarQube
│ PostgreSQL 5432(仅 Docker 内网)
▼
PostgreSQL 17
公网只需要开放:
22/TCP:SSH,最好只允许管理员 IP;80/TCP:Caddy 申请证书和 HTTP 跳转;443/TCP:SonarQube Web、API 与扫描器访问。
不要把 PostgreSQL 的 5432、SonarQube 的 9000 或 Elasticsearch 的内部端口直接暴露公网。本文把 SonarQube 的宿主机端口限制为 127.0.0.1:9000,即使防火墙配置失误,也不会直接开放管理界面。
准备一个子域名,例如:
sonar.example.com → VPS 公网 IPv4
确认 DNS 已解析到 VPS:
dig +short sonar.example.com A
更新 Ubuntu 并安装基础工具:
sudo apt update
sudo apt install -y ca-certificates curl gnupg openssl jq dnsutils
如果还没安装 Docker Engine 与 Compose 插件,先按 Docker 官方仓库完成安装,然后确认版本:
docker version
docker compose version
生产环境建议启用 Swap 作为突发 OOM 的缓冲,但不能把 Swap 当成内存替代品:
swapon --show
free -h
SonarQube Community Build 26.x 使用内置 Elasticsearch 8.x。官方要求主机满足以下最低值:
vm.max_map_count >= 524288;fs.file-max >= 131072;- SonarQube 进程至少可打开 131072 个文件;
- 至少允许 8192 个线程。
先检查当前值:
sysctl vm.max_map_count
sysctl fs.file-max
ulimit -n
ulimit -u
写入持久化 sysctl 配置:
sudo tee /etc/sysctl.d/99-sonarqube.conf >/dev/null <<'EOF'
vm.max_map_count=524288
fs.file-max=131072
EOF
sudo sysctl --system
Compose 中还会给 SonarQube 容器设置 nofile 和 nproc。如果 VPS 使用 OpenVZ、受限 LXC 或某些容器型产品,宿主商可能禁止修改 vm.max_map_count。这种环境不适合运行 SonarQube;购买前先确认能否使用 sysctl,不要用关闭 Elasticsearch 安全检查的方式绕过。
创建独立目录:
sudo mkdir -p /opt/sonarqube
sudo chown -R "$USER":"$USER" /opt/sonarqube
cd /opt/sonarqube
生成数据库密码和 JWT 会话密钥:
POSTGRES_PASSWORD="$(openssl rand -base64 36 | tr -d '\n')"
SONAR_JWT_SECRET="$(openssl rand -base64 32 | tr -d '\n')"
printf 'POSTGRES_PASSWORD=%s\nSONAR_JWT_SECRET=%s\n' \
"$POSTGRES_PASSWORD" "$SONAR_JWT_SECRET" > .env
chmod 600 .env
unset POSTGRES_PASSWORD SONAR_JWT_SECRET
.env 包含生产密钥,不要提交到 Git。备份时也应加密保存,并限制只有管理员能够读取。
创建 compose.yaml:
services:
db:
image: postgres:17-alpine
container_name: sonarqube-db
restart: unless-stopped
environment:
POSTGRES_DB: sonarqube
POSTGRES_USER: sonarqube
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
volumes:
- sonarqube_db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sonarqube -d sonarqube"]
interval: 10s
timeout: 5s
retries: 10
start_period: 20s
networks:
- sonarnet
sonarqube:
image: sonarqube:26.9.0.129388-community
container_name: sonarqube-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
SONAR_JDBC_URL: jdbc:postgresql://db:5432/sonarqube
SONAR_JDBC_USERNAME: sonarqube
SONAR_JDBC_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
SONAR_AUTH_JWTBASE64HS256SECRET: ${SONAR_JWT_SECRET:?set SONAR_JWT_SECRET in .env}
ports:
- "127.0.0.1:9000:9000"
volumes:
- sonarqube_data:/opt/sonarqube/data
- sonarqube_extensions:/opt/sonarqube/extensions
- sonarqube_logs:/opt/sonarqube/logs
- sonarqube_temp:/opt/sonarqube/temp
ulimits:
nofile:
soft: 131072
hard: 131072
nproc: 8192
healthcheck:
test: ["CMD-SHELL", "curl --fail --silent http://localhost:9000/api/system/status | grep -q '\"status\":\"UP\"'"]
interval: 30s
timeout: 10s
retries: 20
start_period: 180s
networks:
- sonarnet
networks:
sonarnet:
name: sonarqube-network
volumes:
sonarqube_db:
sonarqube_data:
sonarqube_extensions:
sonarqube_logs:
sonarqube_temp:
PostgreSQL 14–18 都在当前 Community Build 的支持范围内。这里锁定 PostgreSQL 17,避免数据库大版本随镜像标签漂移。SonarQube 版本同样锁定完整标签,升级必须显式修改。
先让 Compose 展开并检查配置:
docker compose config >/tmp/sonarqube-compose-rendered.yaml
docker compose pull
docker compose up -d
首次启动要初始化数据库和搜索索引,可能需要几分钟:
docker compose ps
docker compose logs -f --tail=200 sonarqube
出现 SonarQube is operational 或 API 返回 UP 后,按 Ctrl+C 退出日志:
curl -fsS http://127.0.0.1:9000/api/system/status | jq
如果这台 VPS 已有 Caddy,可以直接增加站点块;没有时先按官方仓库安装。创建或编辑 /etc/caddy/Caddyfile:
sonar.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:9000 {
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
header_up X-Forwarded-For {remote_host}
}
log {
output file /var/log/caddy/sonarqube-access.log
format json
}
}
检查并加载配置:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager
验证公网 HTTPS:
curl -I https://sonar.example.com
curl -fsS https://sonar.example.com/api/system/status | jq
SonarQube 入站只提供 HTTP,生产 HTTPS 需要由反向代理终止 TLS。Caddy 会传递 Host、X-Forwarded-Proto 和 X-Forwarded-For,这对外部 URL、OAuth/SAML 和审计日志都很重要。更多反向代理实践可参考Caddy 自动 HTTPS 教程。
浏览器访问 https://sonar.example.com,首次可使用默认账号 admin、密码 admin。系统会要求立即修改密码。
登录后至少完成以下设置:
- 修改默认管理员密码,并保存到密码管理器;
- 在
Administration → Configuration → General → General设置 Server base URL; - 检查全局权限,不要让匿名用户拥有不必要的浏览或分析权限;
- 为每个 CI 项目创建独立的 Project Analysis Token;
- 给 Token 设置过期时间并记录轮换责任人;
- 不要让 CI 使用管理员 User Token。
建议的 Base URL:
https://sonar.example.com
Token 只会在创建时显示一次。把它放进 Jenkins Credentials 或 GitHub Actions Secrets,不要写在仓库的 sonar-project.properties、Compose 文件或构建日志中。
在 SonarQube 中创建 Local Project,例如:
Project key: my-api
Display name: My API
Main branch: main
然后生成 Project Analysis Token。项目根目录可创建 sonar-project.properties:
sonar.projectKey=my-api
sonar.projectName=My API
sonar.sources=src
sonar.tests=tests
sonar.sourceEncoding=UTF-8
sonar.javascript.lcov.reportPaths=coverage/lcov.info
不要把 sonar.token 写进这个文件。扫描时通过环境变量传入:
export SONAR_HOST_URL="https://sonar.example.com"
export SONAR_TOKEN="replace-with-project-analysis-token"
docker run --rm \
-e SONAR_HOST_URL \
-e SONAR_TOKEN \
-v "$PWD:/usr/src" \
sonarsource/sonar-scanner-cli:latest
unset SONAR_TOKEN
Scanner 镜像也应在正式流水线中锁定经过验证的版本。这里使用 latest 只用于演示首次验证,不要照搬到长期生产流程。
在 GitHub 仓库的 Settings → Secrets and variables → Actions 添加:
SONAR_HOST_URL:https://sonar.example.com;SONAR_TOKEN:项目级 Analysis Token。
示例 .github/workflows/sonarqube.yml:
name: SonarQube
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Test
run: npm ci && npm test -- --coverage
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@v7
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
云托管 GitHub Runner 必须能够访问 SonarQube 的 HTTPS 域名。如果只允许私网访问,应改用自托管 Runner,并确保 Runner 到 SonarQube 的路由和证书链正常。生产环境还应锁定第三方 Action 的提交 SHA,降低供应链漂移风险。
先在 Jenkins 安装 SonarQube Scanner 插件,并完成两项配置:
Manage Jenkins → Credentials创建 Secret Text,保存项目 Token;Manage Jenkins → System → SonarQube servers添加服务器 URL 与凭据。
示例 Jenkinsfile:
pipeline {
agent any
stages {
stage('Test') {
steps {
sh 'npm ci'
sh 'npm test -- --coverage'
}
}
stage('SonarQube Analysis') {
steps {
withSonarQubeEnv('sonarqube-vps') {
sh 'npx sonar-scanner'
}
}
}
stage('Quality Gate') {
steps {
timeout(time: 10, unit: 'MINUTES') {
waitForQualityGate abortPipeline: true
}
}
}
}
}
waitForQualityGate 依赖 SonarQube 回调 Jenkins。需要在 SonarQube 的项目 Webhook 中填写:
https://jenkins.example.com/sonarqube-webhook/
Webhook 应设置随机 Secret,并在接收端校验 HMAC。配置后主动制造一次失败的质量门禁,确认流水线确实停止;只测试绿色结果,无法证明门禁有效。
内置 Sonar way 默认关注新代码,常见条件包括新问题、安全热点、覆盖率和重复率。对刚接入的老项目,不建议第一天就强制清零全部历史债务,否则团队很容易直接绕过规则。
更实用的策略是:
- 先对 New Code 强制执行质量门禁;
- 新增高严重度问题必须为 0;
- 新代码覆盖率根据项目现状逐步提高;
- 重复率保持在团队可接受范围;
- 安全热点需要明确责任人审查;
- 门禁失败必须让 CI 返回失败,而不是只发通知。
质量门禁不是数字越严越好。条件必须能被团队执行、能解释、能复现,否则它最终只会成为每次发布都被手动忽略的红灯。
如果使用 UFW,只开放 SSH、HTTP 和 HTTPS:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status numbered
确认 9000 与 5432 没有监听公网:
sudo ss -lntp | grep -E ':(443|9000|5432)\b'
预期 9000 只显示 127.0.0.1:9000,5432 不出现在宿主机监听列表。若 SonarQube 只供公司或家庭实验室使用,可进一步把域名放到 WireGuard、Headscale 或 Cloudflare Access 后面;但 CI Runner 必须仍能访问它。
日常排查先看容器状态:
cd /opt/sonarqube
docker compose ps
docker compose logs --tail=200 sonarqube
docker compose logs --tail=200 db
SonarQube 常用日志包括:
web.log:Web 进程、认证、数据库连接;ce.log:后台分析任务;es.log:Elasticsearch 启动、索引和磁盘水位;access.log:Web 访问记录。
通过卷检查日志:
docker exec sonarqube-app ls -lh /opt/sonarqube/logs
docker exec sonarqube-app tail -n 100 /opt/sonarqube/logs/web.log
docker exec sonarqube-app tail -n 100 /opt/sonarqube/logs/es.log
至少监控:CPU、可用内存、Swap、磁盘使用率、磁盘 I/O、容器重启次数、PostgreSQL 连接和 /api/system/status。Elasticsearch 默认在磁盘使用达到高水位后可能停止写入,所以不要等磁盘 100% 才告警。
SonarQube 的权威业务数据在数据库中;Elasticsearch 索引可以重建。官方建议使用数据库自带工具备份。生产备份至少包含:
- PostgreSQL 数据库逻辑备份;
compose.yaml和.env的加密副本;- 已安装插件列表和 extensions 卷;
- Caddy 配置;
- 当前 SonarQube、PostgreSQL 镜像版本;
- 恢复步骤和一次实际演练记录。
创建备份脚本目录:
sudo mkdir -p /opt/backups/sonarqube
sudo chown -R "$USER":"$USER" /opt/backups/sonarqube
执行 PostgreSQL 逻辑备份:
cd /opt/sonarqube
BACKUP_DATE="$(date +%F-%H%M%S)"
docker compose exec -T db \
pg_dump -U sonarqube -d sonarqube -Fc \
> "/opt/backups/sonarqube/sonarqube-${BACKUP_DATE}.dump"
sha256sum "/opt/backups/sonarqube/sonarqube-${BACKUP_DATE}.dump" \
> "/opt/backups/sonarqube/sonarqube-${BACKUP_DATE}.dump.sha256"
备份 extensions 和配置:
docker run --rm \
-v sonarqube_extensions:/source:ro \
-v /opt/backups/sonarqube:/backup \
alpine:3.22 \
tar -czf /backup/sonarqube-extensions.tar.gz -C /source .
tar -czf "/opt/backups/sonarqube/sonarqube-config-${BACKUP_DATE}.tar.gz" \
-C /opt/sonarqube compose.yaml .env
本机备份不算完整备份。再把文件加密同步到另一台服务器或对象存储,并按照VPS 备份恢复演练指南定期验证能否恢复。
恢复前先确认备份版本与目标 SonarQube 版本兼容。不要直接在唯一生产实例上测试。
停止 SonarQube,保留数据库运行:
cd /opt/sonarqube
docker compose stop sonarqube
创建测试数据库或在隔离环境中恢复。以下命令演示恢复到已经清空的 sonarqube 数据库:
docker compose exec -T db dropdb -U sonarqube --if-exists sonarqube
docker compose exec -T db createdb -U sonarqube sonarqube
docker compose exec -T db \
pg_restore -U sonarqube -d sonarqube --clean --if-exists \
< /opt/backups/sonarqube/sonarqube-YYYY-MM-DD-HHMMSS.dump
删除可重建的 Elasticsearch 索引,再启动服务:
docker compose run --rm --entrypoint sh sonarqube \
-c 'rm -rf /opt/sonarqube/data/es8/*'
docker compose up -d
docker compose logs -f --tail=200 sonarqube
恢复成功至少要验证:管理员可以登录、项目和历史分析存在、Token 权限符合预期、一次新扫描成功、Quality Gate 能回传 CI。只看到容器健康不能证明恢复完成。
Community Build 按月更新,不应让 Watchtower 或自动拉取任务直接替换生产容器。标准流程是:
- 阅读当前版本到目标版本之间的发布和升级说明;
- 检查数据库版本、插件和最低资源要求;
- 确保数据库使用率低于 50%,给迁移留空间;
- 创建数据库备份并完成校验;
- 在测试环境用备份副本执行升级;
- 修改 Compose 中的完整镜像标签;
- 拉取镜像并启动;
- 验证项目、扫描、Quality Gate、Webhook 和登录;
- 保留旧镜像与升级前数据库备份,直到观察期结束。
升级命令示例:
cd /opt/sonarqube
docker compose pull sonarqube
docker compose up -d sonarqube
docker compose logs -f --tail=200 sonarqube
数据库发生迁移后,回滚通常不能只把镜像标签改回去,还要恢复升级前的数据库备份。不要把“旧容器还能启动”误认为完整回滚方案。
日志出现类似内容:
max virtual memory areas vm.max_map_count is too low
检查并修复:
sysctl vm.max_map_count
sudo sysctl -w vm.max_map_count=524288
sudo sysctl --system
如果重启后恢复旧值,说明持久化配置没有生效;如果提示权限不足,联系 VPS 厂商确认虚拟化限制。
先查看退出原因和容器日志:
docker inspect sonarqube-app --format '{{.State.Status}} {{.State.ExitCode}} {{.State.OOMKilled}}'
docker compose logs --tail=300 sonarqube
free -h
dmesg -T | grep -i -E 'out of memory|killed process' | tail -n 30
常见原因包括内存不足、内核限制不达标、数据库未就绪、卷权限异常或错误插件。不要靠无限重启掩盖 OOM。
检查数据库健康和连接日志:
docker compose ps db
docker compose logs --tail=200 db
docker compose exec db pg_isready -U sonarqube -d sonarqube
确认 .env 中的密码没有被 shell 特殊字符错误展开,并用 docker compose config 检查渲染结果。不要为了调试把 5432 暴露公网。
从 Runner 所在机器测试:
curl -v https://sonar.example.com/api/system/status
curl -I https://sonar.example.com/batch/index
重点检查 DNS、证书链、出站代理、Caddy 日志和 Runner 防火墙。自签证书需要把 CA 正确加入 Scanner 的 Java Truststore;更简单的生产方案通常是使用受信任 CA 证书。
检查:
- SonarQube Webhook URL 是否以
/sonarqube-webhook/结尾; - SonarQube 能否访问 Jenkins;
- Jenkins 反向代理是否正确传递 Host 与 HTTPS;
- Webhook 最近一次投递状态和响应内容;
- SonarQube 与 Jenkins 插件版本是否兼容。
可以先在 Jenkins 访问日志中确认 Webhook 是否到达,再检查 Pipeline。不要盲目增加 waitForQualityGate 超时时间。
先定位占用:
df -h
docker system df
docker exec sonarqube-app du -sh /opt/sonarqube/data /opt/sonarqube/logs /opt/sonarqube/extensions
docker exec sonarqube-db du -sh /var/lib/postgresql/data
历史分析、数据库、日志、插件和 Docker 镜像都会增长。不要对 SonarQube 数据卷直接执行随意删除;先按保留策略清理历史,再做备份和容量扩容。
- 域名解析到正确 VPS,HTTPS 证书有效;
-
vm.max_map_count、fs.file-max、nofile和nproc达到要求; - SonarQube 与 PostgreSQL 使用锁定版本;
- PostgreSQL 只在 Docker 内网,9000 只绑定 127.0.0.1;
- 默认管理员密码已修改,Server base URL 正确;
- CI 使用项目级 Token,Token 有过期和轮换计划;
- 一次真实扫描成功,并能在 SonarQube 中看到结果;
- 人为制造失败条件后,Quality Gate 能阻止流水线;
- Webhook Secret 已配置并验证;
- 数据库备份已传到异机,并完成过恢复演练;
- 磁盘、内存、健康状态和容器重启次数已有告警;
- 升级流程明确禁止自动漂移镜像标签。
可以作为官方小规模起点,但 SonarQube 与 PostgreSQL 同机后余量不大。个人仓库、低频扫描可以使用;多人高频扫描建议从 4 核 8GB 和 100GB NVMe 起步,并监控 OOM、I/O 与磁盘水位。
H2 适合测试,不适合生产。正式部署应使用受支持的 PostgreSQL、SQL Server 或 Oracle。本文选择 PostgreSQL,因为备份工具成熟、容器部署直接,并且 Community Build 26.2 起支持 PostgreSQL 14–18。
不需要。本文只绑定 127.0.0.1:9000,公网通过 Caddy 的 443 访问。这样可以统一 HTTPS、访问日志和安全策略,并避免管理端口裸露。
免费版与商业版的分支、Pull Request 和 DevOps 集成功能范围会变化,应以当前官方功能对比为准。即使只分析主分支,也可以在 CI 中执行扫描和 Quality Gate;采购或迁移前要确认所需功能所在版本。
不够。SonarQube 官方把数据库备份作为核心,Elasticsearch 索引可重建。至少要备份 PostgreSQL、配置、密钥和插件,并在隔离环境恢复验证。没有恢复演练的备份只能算“可能有用的文件”。
不建议。升级可能包含数据库迁移、插件兼容变化和新的主机要求。应锁定完整镜像标签,先备份和测试,再人工升级;需要回滚时通常还要恢复升级前数据库。
在 VPS 上搭建 SonarQube,真正影响稳定性的不是那一条 docker compose up -d,而是四件事:满足 Elasticsearch 主机限制、给 Java 与数据库留足资源、把管理入口放到 HTTPS 反向代理后面,以及把数据库恢复纳入日常运维。
小团队可以从 4 核 8GB、PostgreSQL 17、SonarQube Community Build 26.9 和 Caddy 开始。先接入一个真实项目,验证扫描、失败门禁、Webhook、备份和恢复,再逐步扩大仓库与团队规模。这样搭出来的才是可用的代码质量平台,而不是一个暂时能打开的面板。
