Chapter 28Lesson 02~165 minutes

Database Backup, Restore, Disaster Recovery, and Data Protection: Guided Hands-On Workflow

Build a disposable PostgreSQL-backed Community Build lab, take a consistent hot database backup, restore it into an isolated target, prove the recovery point, and validate with a fresh analysis.

Backup & RestorePostgreSQLDisaster RecoveryRPO / RTOData Protection

Learning objectives

  • Run a disposable SonarQube Community Build 26.9 instance backed by PostgreSQL 17.11.
  • Take and integrity-check a hot PostgreSQL logical backup without stopping the source SonarQube instance.
  • Create a post-backup analysis marker so the recovery point is observable rather than theoretical.
  • Restore the archive into a separate PostgreSQL container and start a fresh isolated SonarQube instance with empty search state.
  • Prove the restored project/history, then run a fresh scanner analysis and follow its ceTaskId to completion.
  • Keep the source stack intact until the restored stack passes validation and cleanup is explicitly authorized.

1. Lab contract: two isolated stacks, one deliberate recovery point

Disposable resources only. Every container, volume, project key, credential, port, and source file in this lesson is prefixed for Chapter 28. Do not substitute a production database, production SonarQube URL, or proprietary repository.
Component Source Restore target
SonarQube sonarqube:26.9.0.129388-community, port 9100 Same exact image, port 9200
PostgreSQL postgres:17.11-alpine Separate postgres:17.11-alpine
Database sonar sonar_restore
Project key sq28-dr-lab
Source revision baseline before backup, then post-backup marker backup history first, current revision only after validation scan
Search state normal source indexes fresh restore volume; rebuild from restored DB

2. Resource and credential preflight

mkdir -p sq28-dr-lab/{src,evidence/{baseline,backup,post-backup,restore,validation}}
cd sq28-dr-lab

docker --version
docker compose version
sonar-scanner --version
git --version

# Ports must be unused before the lab.
# Linux/macOS example; on Windows use Get-NetTCPConnection or netstat.
( ! command -v ss >/dev/null ) || ( ! ss -ltn | grep -E ':(9100|9200) ' )

df -h . || true
printf '%s\n' 'No production URL, DB host, token, or repository is allowed in this lab.'

Use a disposable local admin password when SonarQube first starts. Later, create project-analysis tokens through the UI and keep them only in shell environment variables. Do not add any token or database password to Git.

3. Define the source and restore stacks

The restore target is separate at both the database and SonarQube layers. The only shared object is the backup archive written to the host evidence directory.

# compose.yml
services:
  pg-source:
    image: postgres:17.11-alpine
    container_name: sq28-pg-source
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: only-for-disposable-lab
      POSTGRES_DB: sonar
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U sonar -d sonar"]
      interval: 5s
      timeout: 3s
      retries: 30
    volumes:
      - sq28_pg_source:/var/lib/postgresql/data

  sonar-source:
    image: sonarqube:26.9.0.129388-community
    container_name: sq28-sonar-source
    depends_on:
      pg-source:
        condition: service_healthy
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://pg-source:5432/sonar
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: only-for-disposable-lab
    ports:
      - "9100:9000"
    volumes:
      - sq28_sonar_source_data:/opt/sonarqube/data
      - sq28_sonar_source_logs:/opt/sonarqube/logs
      - sq28_sonar_source_extensions:/opt/sonarqube/extensions

  pg-restore:
    image: postgres:17.11-alpine
    container_name: sq28-pg-restore
    environment:
      POSTGRES_USER: sonar
      POSTGRES_PASSWORD: only-for-disposable-lab
      POSTGRES_DB: sonar_restore
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U sonar -d sonar_restore"]
      interval: 5s
      timeout: 3s
      retries: 30
    volumes:
      - sq28_pg_restore:/var/lib/postgresql/data

  sonar-restore:
    image: sonarqube:26.9.0.129388-community
    container_name: sq28-sonar-restore
    depends_on:
      pg-restore:
        condition: service_healthy
    environment:
      SONAR_JDBC_URL: jdbc:postgresql://pg-restore:5432/sonar_restore
      SONAR_JDBC_USERNAME: sonar
      SONAR_JDBC_PASSWORD: only-for-disposable-lab
    ports:
      - "9200:9000"
    volumes:
      - sq28_sonar_restore_data:/opt/sonarqube/data
      - sq28_sonar_restore_logs:/opt/sonarqube/logs
      - sq28_sonar_restore_extensions:/opt/sonarqube/extensions

volumes:
  sq28_pg_source: {}
  sq28_sonar_source_data: {}
  sq28_sonar_source_logs: {}
  sq28_sonar_source_extensions: {}
  sq28_pg_restore: {}
  sq28_sonar_restore_data: {}
  sq28_sonar_restore_logs: {}
  sq28_sonar_restore_extensions: {}
docker compose -p sq28dr config > evidence/compose-rendered.yml
docker compose -p sq28dr up -d pg-source sonar-source

# Wait until the source server reports UP.
until curl -fsS http://localhost:9100/api/system/status | grep -q '"status":"UP"'; do sleep 5; done
curl -fsS http://localhost:9100/api/system/status | tee evidence/source-system-status.json

4. Create one synthetic project and baseline analysis

cat > src/app.py <<'PY'
def normalize(name: str) -> str:
    return " ".join(name.strip().split()).lower()
PY

cat > sonar-project.properties <<'EOF'
sonar.projectKey=sq28-dr-lab
sonar.projectName=SQ Chapter 28 DR Lab
sonar.sources=src
sonar.sourceEncoding=UTF-8
EOF

cat > .gitignore <<'EOF'
.scannerwork/
evidence/
EOF

git init
git config user.email learner@example.invalid
git config user.name "SonarQube Learner"
git add src sonar-project.properties .gitignore
git commit -m "chapter28 baseline"
git rev-parse HEAD | tee evidence/baseline/revision.txt

Open http://localhost:9100, change the default admin password, create project sq28-dr-lab, and create a disposable project analysis token. Export it as SONAR_TOKEN_SOURCE without printing it.

export SONAR_HOST_URL=http://localhost:9100
export SONAR_TOKEN="$SONAR_TOKEN_SOURCE"
sonar-scanner -Dsonar.projectVersion=1.0.0 2>&1 | tee evidence/baseline/scanner.log
cp .scannerwork/report-task.txt evidence/baseline/report-task.txt
BASE_TASK="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
echo "$BASE_TASK" > evidence/baseline/ceTaskId.txt
curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
  "$SONAR_HOST_URL/api/ce/task?id=$BASE_TASK" > evidence/baseline/ce-task.json

Poll until the task is terminal before taking the backup. This makes the baseline analysis part of the recovery point rather than an in-flight ambiguous task.

5. Take and verify a hot PostgreSQL backup

Keep sonar-source running. Current SonarQube guidance supports hot database backups, and PostgreSQL pg_dump gives a consistent logical snapshot.

date -u +%FT%TZ | tee evidence/backup/started-at.txt

docker compose -p sq28dr exec -T pg-source \
  pg_dump -U sonar -d sonar --format=custom \
  > evidence/backup/sonarqube.dump

date -u +%FT%TZ | tee evidence/backup/completed-at.txt
sha256sum evidence/backup/sonarqube.dump | tee evidence/backup/sonarqube.dump.sha256

# Verify the archive is readable before calling it a backup candidate.
docker run --rm -i postgres:17.11-alpine \
  pg_restore --list \
  < evidence/backup/sonarqube.dump \
  > evidence/backup/archive-list.txt

test -s evidence/backup/archive-list.txt

Copy the dump and checksum to storage outside the source failure domain in a real design. The local lab keeps the file in evidence/ only so the entire exercise remains disposable.

6. Create an observable post-backup marker

Now change source after the backup. The restore target should not contain this second analysis until you deliberately run a validation scan against it.

cat >> src/app.py <<'PY'

def display_label(name: str) -> str:
    return normalize(name).title()
PY

git add src/app.py
git commit -m "post-backup marker"
git rev-parse HEAD | tee evidence/post-backup/revision.txt

export SONAR_HOST_URL=http://localhost:9100
export SONAR_TOKEN="$SONAR_TOKEN_SOURCE"
sonar-scanner -Dsonar.projectVersion=2.0.0 2>&1 | tee evidence/post-backup/scanner.log
cp .scannerwork/report-task.txt evidence/post-backup/report-task.txt
POST_TASK="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
echo "$POST_TASK" > evidence/post-backup/ceTaskId.txt

Confirm version 2.0.0 appears on the source server. This is intentional data that is newer than the recovery point.

7. Restore into the isolated PostgreSQL target

docker compose -p sq28dr up -d pg-restore
until docker compose -p sq28dr exec -T pg-restore pg_isready -U sonar -d sonar_restore >/dev/null; do sleep 2; done

# Re-check the dump immediately before restore.
sha256sum -c evidence/backup/sonarqube.dump.sha256

# Restore into the empty isolated database. No source DB is modified.
docker compose -p sq28dr exec -T pg-restore \
  pg_restore -U sonar -d sonar_restore \
  --no-owner --no-privileges --exit-on-error \
  < evidence/backup/sonarqube.dump \
  2>&1 | tee evidence/restore/pg-restore.log

docker compose -p sq28dr exec -T pg-restore \
  psql -U sonar -d sonar_restore -Atc 'select current_database(), version();' \
  | tee evidence/restore/database-identity.txt

At this point the source remains running and untouched. The target database is isolated and contains only state up to the backup snapshot.

8. Start a fresh restore server and let search state rebuild

The restore SonarQube service uses a fresh sq28_sonar_restore_data volume, so it begins without old Elasticsearch indexes. This is the Docker equivalent of the documented restore rule: restored DB plus cleared/fresh data/es8.

docker compose -p sq28dr up -d sonar-restore

docker compose -p sq28dr logs --no-color sonar-restore \
  | tee evidence/restore/sonar-startup.log

until curl -fsS http://localhost:9200/api/system/status | grep -q '"status":"UP"'; do sleep 5; done
curl -fsS http://localhost:9200/api/system/status | tee evidence/restore/system-status.json

Log in with the disposable local administrator state that existed before the backup. Verify that sq28-dr-lab exists and that its Activity history contains the baseline recovery point but not the post-backup version 2.0.0 analysis. That absence is expected and demonstrates the lab's RPO boundary.

9. Run a fresh post-restore analysis and preserve ceTaskId

After the historical state is verified, create a new restore-only project-analysis token in the restored project. Do not reuse this token anywhere else.

export SONAR_HOST_URL=http://localhost:9200
export SONAR_TOKEN="$SONAR_TOKEN_RESTORE"

git rev-parse HEAD | tee evidence/validation/revision.txt
sonar-scanner -Dsonar.projectVersion=3.0.0-dr-validated \
  2>&1 | tee evidence/validation/scanner.log
cp .scannerwork/report-task.txt evidence/validation/report-task.txt
VALIDATION_TASK="$(sed -n 's/^ceTaskId=//p' .scannerwork/report-task.txt)"
echo "$VALIDATION_TASK" | tee evidence/validation/ceTaskId.txt

curl -fsS -H "Authorization: Bearer $SONAR_TOKEN" \
  "$SONAR_HOST_URL/api/ce/task?id=$VALIDATION_TASK" \
  | tee evidence/validation/ce-task.json

Wait for SUCCESS, then verify the current Git revision, project measures/issues, and Quality Gate. A successful scanner upload is not enough; the restored server must process the task and expose coherent project state.

10. Challenge: identify the owning recovery layer

The database archive checksum verifies and pg_restore succeeds, but the restore SonarQube container cannot start because its JDBC URL points to sonar_restore_typo. Should you retake the backup?

No. The backup and database restore evidence are already sound. Preserve the SonarQube startup error, prove that PostgreSQL contains sonar_restore, correct only the restore server's JDBC target, and restart that disposable server. This is a deployment-configuration failure, not evidence of database-backup corruption.

Knowledge check

Why create an analysis after the backup before performing the restore?

Why is pg_restore --list useful before restore?

Why does the restore server use a fresh search-data volume?

Why keep the source stack running until restore validation finishes?

Scanner exit 0 on the restore target proves what, and what does it not prove?

Next lesson

Choose a recovery architecture from RPO, RTO, and ownership

Lesson 3 turns the lab into design patterns for database protection, configuration, search state, security, cadence, and recovery objectives.

Official references and version notes

Version and compatibility note

Rechecked 2026-09-08. Mandatory examples target SonarQube Community Build 26.9.0.129388, the official image sonarqube:26.9.0.129388-community, PostgreSQL 17.11 via postgres:17.11-alpine, and SonarScanner CLI 8.1.0.6389. Community Build 26.9 supports PostgreSQL 14–18. The lab restores a 26.9 database into a fresh 26.9 instance first; it does not combine disaster recovery with a SonarQube upgrade or database-vendor migration. Search indexes are treated as rebuildable Sonar-owned state, not the backup authority. Recheck current database requirements, update path, plugins, Java/runtime requirements, and restore instructions before applying these procedures to another release.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.