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.
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.
2. Predict the topology before creating it
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
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?
The PostgreSQL container/volume, SonarQube named volumes, and private network should still exist. Only the replaceable SonarQube container object was removed.
Why is PostgreSQL port 5432 not published to the host in this lab?
SonarQube reaches PostgreSQL over the private Docker network. Host publication is unnecessary and would widen the database attack surface.
What does the persistence proof demonstrate—and what does it not?
It shows the same durable DB-backed SonarQube state survives replacement of the SonarQube container. It does not prove backup/restore, disaster recovery, or cross-version upgrade safety.
If SonarQube cannot resolve db, should you change
the quality gate?
No. That is a Docker network/DNS failure, not a project-policy failure.
Why is docker compose down -v dangerous on a
persistent stack?
The -v option removes named volumes associated with
the Compose project and can destroy database/application state.
Official references and version notes
- SonarQube Community Build downloads — current Community Build release identity.
- Official SonarQube Docker image tags — current official image tags and digests.
- Prepare the Docker installation — pre-installation checks and named-volume guidance.
- Set up and start your container — docker run/Compose, JDBC environment variables, ports, and volume warnings.
- Linux pre-installation — embedded-search host limits and writable /tmp requirements.
- Configuration methods — preferred Docker environment-variable configuration model.
- System properties — current JDBC and web-server property names.
- Installing database — supported database versions and H2 non-production boundary.
- Updating Community Build — container recreation and persistent-state upgrade guidance.
- SonarQube official image overview — official image usage, host prerequisites, port and edition tags.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.