LPIC-3 Virtualization and Containerization study materials
LPIC-3 Virtualization and Containerization study resources

A 2.9 GB Image, a Leaked Secret, and a 19-Minute Build

LPIC-3 Virtualization and Containerization · Put your knowledge to work in context.

All case studies

A 2.9 GB Image, a Leaked Secret, and a 19-Minute Build

A team builds a Java Spring Boot service with a single-stage Dockerfile. The build context is the repository root, which contains .git (410 MB), target/ (300 MB), and node_modules/ (280 MB) for a frontend module. The Dockerfile copies everything with COPY . ., runs mvn -B package, and passes a database password as ARG DB_PASSWORD with --build-arg DB_PASSWORD=... in CI. The resulting image is 2.9 GB, a clean build takes 19 minutes on the CI runner, and every source change invalidates the Maven dependency download. A security scan reports the password value in the image history and a .git directory inside the image. The team wants weekly rebuilds, a non-root runtime, and reproducible builds, but cannot change the application's base OS family (Debian 12) this quarter.

1. Explain why the DB_PASSWORD still appears in the image history and why a later RUN rm cannot remove it.

Read the worked answer

A Docker image is an ordered stack of immutable layers. Each instruction produces a layer, and the image config records the instruction metadata. An ARG DB_PASSWORD that is consumed by a RUN is visible in that layer's history and in the image configuration; docker history --no-trunc will print the build argument. A later RUN rm -f /root/.m2/settings.xml does not erase the earlier layer. It creates a new layer containing a whiteout entry, while the original file and the secret remain in the lower layer and are recoverable with docker save image | tar -x.

Multi-stage builds only help if the secret is confined to a stage that is not copied forward. They do not help if the secret is in the final stage's history. The correct fix is a BuildKit secret mount:

RUN --mount=type=secret,id=db_password,target=/run/secrets/db_password \
    mvn -B -q package -DskipTests

Build with docker buildx build --secret id=db_password,src=./db_password.txt .... The secret is mounted as a tmpfs and is not committed to a layer or to history. The longer-term fix is to fetch the credential at runtime from a secret manager or Kubernetes Secret, and never put it in ENV either, because environment variables are visible in docker inspect.

2. Rewrite the build with multi-stage and BuildKit features. Show the key Dockerfile lines and the build command.

Read the worked answer

Split dependency resolution from source compilation, and copy only the artifact into the runtime stage.

# syntax=docker/dockerfile:1.7
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /src
COPY pom.xml .
RUN --mount=type=cache,id=m2,target=/root/.m2 \
    mvn -B -q dependency:go-offline
COPY src ./src
RUN --mount=type=cache,id=m2,target=/root/.m2 \
    --mount=type=secret,id=db_password \
    mvn -B -q package -DskipTests

FROM eclipse-temurin:21-jre-jammy
RUN groupadd -g 10001 app && useradd -u 10001 -g app -m app
WORKDIR /app
COPY --from=build /src/target/app.jar /app/app.jar
USER 10001:10001
ENTRYPOINT ["java","-jar","/app/app.jar"]

Add a .dockerignore containing .git, target, node_modules, and *.log. That reduces the build context from roughly 1 GB to tens of megabytes and removes the Git history from the image. The Maven cache mount survives across builds, so a source-only change recompiles instead of downloading dependencies again. The runtime stage drops Maven, the JDK, source, and the frontend tree; expect roughly 300 MB instead of 2.9 GB and a clean build in the 4-6 minute range.

Build and push with:

docker buildx build --secret id=db_password,src=./db_password.txt \
  -t registry.example/app:1.4.2 --push .

Pin the base images by digest for repeatability. The secret file should be supplied by CI, not committed.

3. What checks confirm the image no longer contains the secret or the Git history, and how reproducible is the result?

Read the worked answer

Check history and contents directly: docker history --no-trunc registry.example/app:1.4.2 should show no password value; docker inspect --format '{{json .Config.Env}}' should show no credential; docker run --rm registry.example/app:1.4.2 find / -name .git -o -name settings.xml should return nothing relevant; and docker save image | tar -tv | grep -i password should not find a leaked file. Run a secret scanner such as trivy image --scanners secret or gitleaks in CI, and fail the build on a hit. Confirm non-root with docker run --rm image id, which should print uid=10001.

Reproducibility is a separate axis. Pin both base images by digest. Set Maven's project.build.outputTimestamp from the commit time and pass SOURCE_DATE_EPOCH to the build; BuildKit's timestamp rewriting can normalize layer timestamps on recent versions. Then build twice from a clean cache and compare the resulting image digest with docker buildx imagetools inspect. Without timestamp normalization the jar and layers will differ even with identical inputs, so 'reproducible' here means identical dependency versions and base digests, not necessarily bit-identical images. Document which guarantee you are claiming.