Chapter 04Lesson 02~150 minutes

Docker, Containers, Persistent Storage, and Production Deployment: Guided Hands-On Workflow

Build an isolated Community Build and PostgreSQL stack, inspect every boundary, recreate the SonarQube container, prove persistence, and tear down only guarded lab resources.

docker runPostgreSQLNamed volumesPersistence proofEvidence

Learning objectives

  • Preflight Docker, host resources, image identity, ports, and lab names before mutation.
  • Create an isolated private network, PostgreSQL volume, and SonarQube data/extensions/log volumes using bounded local resources.
  • Start PostgreSQL and SonarQube with an exact image version and explicit JDBC configuration.
  • Verify logs, mounts, DNS/network membership, system status, and a durable project/configuration artifact.
  • Remove and recreate only the SonarQube container, prove state survives, then perform a guarded teardown without accidental volume deletion.

1. Lab contract and stop conditions

The lab is local-only and disposable. It uses sonarqube:26.9.0.129388-community, postgres:17.11, one private network, four named volumes, and loopback port 127.0.0.1:9000. It does not require a public DNS name, reverse proxy, TLS certificate, CI provider, Kubernetes, or commercial SonarQube edition.

Stop immediately if the chosen container/network/volume names already contain non-lab data, if port 9000 is owned by an unknown process, if disk/memory headroom is inadequate, or if Docker points to a shared/production daemon. Do not run cleanup commands until resource names have been re-listed and verified.

2. Predict the topology before creating it

Disposable container lab
flowchart LR
B[Browser on host] -->|127.0.0.1:9000| SQ[container: sq-lab]
SQ -->|Docker DNS: db:5432| DB[container: sq-db]
SQ --- D[(sq_data)]
SQ --- E[(sq_extensions)]
SQ --- L[(sq_logs)]
DB --- P[(sq_pgdata)]
SQ --- N{{sq_net}}
DB --- N

Prediction 1: removing sq-lab will remove that container object but not the named volumes or PostgreSQL container. Prediction 2: recreating sq-lab on the same network with the same JDBC configuration should reconnect to the same durable SonarQube database.

3. Read-only preflight

docker version
docker info --format 'CPUs={{.NCPU}} Memory={{.MemTotal}}'
docker ps -a --filter name=sq-lab --filter name=sq-db
docker network ls --filter name='^sq_net$'
docker volume ls --filter name='sq_'
# Linux host only:
sysctl vm.max_map_count
sysctl fs.file-max
ulimit -n
ulimit -u

Also verify that TCP 9000 is free. On PowerShell use Get-NetTCPConnection -State Listen -LocalPort 9000 -ErrorAction SilentlyContinue; on Linux use ss -ltn | grep ':9000' || true.

4. Pull and record exact image identities

docker pull sonarqube:26.9.0.129388-community
docker pull postgres:17.11
docker image inspect sonarqube:26.9.0.129388-community --format '{{json .RepoDigests}}'
docker image inspect postgres:17.11 --format '{{json .RepoDigests}}'

Save the outputs. The SonarQube tag is pinned, but the evidence packet should still record the resolved digest. This makes a later “same tag, different platform/artifact” question answerable.

5. Create only guarded lab resources

docker network create sq_net
docker volume create sq_data
docker volume create sq_extensions
docker volume create sq_logs
docker volume create sq_pgdata

Re-list them immediately with docker network inspect sq_net and docker volume inspect sq_data sq_extensions sq_logs sq_pgdata. The names are intentionally specific and will be used as cleanup guards later.

6. Start PostgreSQL privately

Use fake local credentials. The password below exists only for this isolated lab and must never be reused.

docker run -d --name sq-db \
  --network sq_net \
  --network-alias db \
  -e POSTGRES_DB=sonar \
  -e POSTGRES_USER=sonar \
  -e POSTGRES_PASSWORD=FAKE_LOCAL_SQ_DB_ONLY \
  -v sq_pgdata:/var/lib/postgresql/data \
  postgres:17.11

Do not publish port 5432. Verify readiness through the container itself:

docker logs --tail 80 sq-db
docker exec sq-db pg_isready -U sonar -d sonar

7. Start SonarQube with explicit network and volumes

docker run -d --name sq-lab \
  --network sq_net \
  -p 127.0.0.1:9000:9000 \
  -e SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonar \
  -e SONAR_JDBC_USERNAME=sonar \
  -e SONAR_JDBC_PASSWORD=FAKE_LOCAL_SQ_DB_ONLY \
  -v sq_data:/opt/sonarqube/data \
  -v sq_extensions:/opt/sonarqube/extensions \
  -v sq_logs:/opt/sonarqube/logs \
  sonarqube:26.9.0.129388-community

Why these arguments exist: the network provides private DNS; loopback-only publishing prevents broad host exposure; JDBC variables select the external database; and the three named SonarQube volumes follow the current official Docker preparation guidance.

8. Observe startup by layer

docker logs --tail 160 sq-lab
docker inspect sq-lab --format '{{json .Mounts}}'
docker inspect sq-lab --format '{{json .NetworkSettings.Networks}}'
docker stats --no-stream sq-lab sq-db
curl -sS http://127.0.0.1:9000/api/system/status

Wait for the server to become operational; do not substitute repeated restarts for diagnosis. If startup fails, preserve the first logs and inspect the owning layer: DB readiness/DNS, mount permissions, host search prerequisites, memory pressure, or image/config mismatch.

9. Create one durable marker without turning this into an API chapter

Open http://127.0.0.1:9000. Complete the required bootstrap administrator password change for this disposable lab, then create a manual local project named SQ Container Persistence Lab with key sq-container-persistence-lab. No scanner run is required in this chapter.

Record a screenshot or written evidence showing the project exists. The project metadata and changed administrator state live in the PostgreSQL-backed SonarQube application state, not in the replaceable container object.

10. Recreate SonarQube and prove persistence

docker stop sq-lab
docker rm sq-lab
# Verify volumes and database still exist BEFORE recreation
docker ps --filter name=sq-db
docker volume ls --filter name='sq_'

docker run -d --name sq-lab \
  --network sq_net \
  -p 127.0.0.1:9000:9000 \
  -e SONAR_JDBC_URL=jdbc:postgresql://db:5432/sonar \
  -e SONAR_JDBC_USERNAME=sonar \
  -e SONAR_JDBC_PASSWORD=FAKE_LOCAL_SQ_DB_ONLY \
  -v sq_data:/opt/sonarqube/data \
  -v sq_extensions:/opt/sonarqube/extensions \
  -v sq_logs:/opt/sonarqube/logs \
  sonarqube:26.9.0.129388-community

After /api/system/status returns UP, log in using the changed lab administrator password and verify that sq-container-persistence-lab still exists. This demonstrates state continuity across container replacement. It is not a backup/restore test and does not prove disaster recovery.

11. Challenge: choose the owning layer

Without changing anything, decide where you would look first for each symptom:

Symptom First owner Evidence
UnknownHostException: db Docker network/DNS docker network inspect sq_net
PostgreSQL accepts no connection DB readiness/credential pg_isready + DB logs
Search bootstrap check fails Host/search prerequisite SonarQube logs + host sysctl/ulimit
Project disappears after only SonarQube recreation Database identity/persistence JDBC endpoint + DB volume/container identity

12. Guarded teardown

Never paste teardown into an unknown Docker daemon. First re-list the exact lab resources and confirm they contain nothing valuable.
docker ps -a --filter name=sq-lab --filter name=sq-db
docker volume ls --filter name='sq_'
docker network inspect sq_net --format '{{json .Containers}}'

# Only after manual verification:
docker stop sq-lab sq-db
docker rm sq-lab sq-db
docker network rm sq_net
docker volume rm sq_data sq_extensions sq_logs sq_pgdata

Do not use docker system prune, docker volume prune, or docker compose down -v as a routine cleanup shortcut. SonarSource’s update guidance explicitly warns that volume-removal operations can destroy database state.

Knowledge check

What should exist after docker rm sq-lab but before recreation?

Why is PostgreSQL port 5432 not published to the host in this lab?

What does the persistence proof demonstrate—and what does it not?

If SonarQube cannot resolve db, should you change the quality gate?

Why is docker compose down -v dangerous on a persistent stack?

Next lesson

From a working lab to repeatable deployment design

Lesson 3 turns the lab into design choices: Compose versus ad-hoc run commands, named volumes versus bind mounts, runtime secret injection, digest pinning, resource/security boundaries, and production topology.

Official references and version notes

Version and compatibility note

Version-sensitive statements were rechecked against current SonarSource and Docker Hub primary material on 2026-09-07. Mandatory examples use SonarQube Community Build 26.9.0.129388 with the official sonarqube:26.9.0.129388-community image and PostgreSQL postgres:17.11, which is inside the currently supported PostgreSQL 14–18 range. The recorded multi-platform SonarQube image index digest at authoring time is sha256:62930c7f510534bb2bf551ca69ad3ee6f8e12b4116d394597c64cefccbb3313b; learners should inspect the digest they actually pull and retain it in their evidence packet. The SonarQube image supplies its own Java runtime; record java -version inside the running container when runtime provenance matters. No SonarScanner execution, third-party plugin, CI/provider integration, enterprise identity provider, or commercial feature is required by the mandatory Chapter 04 labs. Commercial Server/Data Center, Kubernetes, managed databases, paid CI, and enterprise identity are optional/later-course boundaries.

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.