Chapter 28Lesson 02~155 minutes

Secrets and Configuration Patterns, Runtime Injection, Environment Risk, Build Secrets, and External Secret Stores: Guided Hands-On Workflow and Core Operations

Build a disposable lab that contrasts environment exposure with BuildKit secret mounts, read-only runtime secret files, Compose secret grants, inspection evidence, and rotation.

Build secretsCompose secretsInspectionRotationDisposable lab

Learning objectives

  • Create a disposable project that keeps fake credentials out of the build context and final image.
  • Use BuildKit --secret with a required secret mount and verify the final image does not expose the value.
  • Use a Compose runtime secret file with per-service access and inspect environment/mount evidence without printing the secret.
  • Rotate the runtime value and prove which lifecycle action activates the new version.

1. Lab contract and preflight

This lab uses only synthetic values under a disposable directory. It does not configure a production registry, cloud provider, daemon, or external secret manager. The “provider” is a local directory whose only job is to model an ownership boundary separate from the build context.

set -eu
LAB="$PWD/docker-ch28-lab"
rm -rf "$LAB"
mkdir -p "$LAB/app" "$LAB/secrets" "$LAB/provider-store" "$LAB/evidence"
cd "$LAB"

docker version > evidence/docker-version.txt
docker context inspect "$(docker context show)" > evidence/context.json
docker info > evidence/docker-info.txt
docker buildx version > evidence/buildx-version.txt
docker buildx inspect --bootstrap > evidence/builder.txt
docker compose version > evidence/compose-version.txt
Credential rule: the two values created next are fake lab strings. Never replace them with a real production token while following a tutorial or recording terminal output.

2. Create fake sources and exclude them from ordinary build input

printf '%s
' 'LAB-BUILD-TOKEN-v1-not-real' > secrets/build-token.txt
printf '%s
' 'LAB-RUNTIME-TOKEN-v1-not-real' > provider-store/runtime-token.txt
chmod 600 secrets/build-token.txt provider-store/runtime-token.txt 2>/dev/null || true

cat > .dockerignore <<'EOF'
secrets/
provider-store/
evidence/
.env
*.key
*.pem
EOF

.dockerignore prevents normal context transfer of those paths. The explicit BuildKit secret channel is separate: the client reads the named source file and passes it through the BuildKit session instead of making it available to COPY . ..

3. Build a tiny application without persisting the build token

cat > app/app.sh <<'EOF'
#!/bin/sh
set -eu
if [ -s /run/secrets/runtime_token ]; then
  echo 'runtime-secret-present=yes'
else
  echo 'runtime-secret-present=no'
  exit 42
fi
while :; do sleep 30; done
EOF
chmod +x app/app.sh

cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22.1
RUN --mount=type=secret,id=build_token,required=true     test -s /run/secrets/build_token &&     printf '%s
' 'build-authenticated=yes' > /build-status
RUN addgroup -S app && adduser -S -G app app
COPY --chown=app:app app/app.sh /usr/local/bin/app.sh
USER app
ENTRYPOINT ["/usr/local/bin/app.sh"]
EOF

docker buildx build   --progress=plain   --secret id=build_token,src=secrets/build-token.txt   --load -t da-ch28:lab . 2>&1 | tee evidence/build.log

required=true makes absence explicit. The secret is available only to that RUN. The build produces a harmless status file, not a copy of the token.

4. Verify absence from image metadata and filesystem

docker image inspect da-ch28:lab > evidence/image-inspect.json
docker history --no-trunc da-ch28:lab > evidence/history.txt

docker image inspect da-ch28:lab --format '{{json .Config.Env}}'
docker run --rm --entrypoint sh da-ch28:lab -c   'test -f /build-status && cat /build-status; test ! -e /run/secrets/build_token'

# Search only for the synthetic marker, never a real secret.
if grep -R -F 'LAB-BUILD-TOKEN-v1-not-real' evidence/image-inspect.json evidence/history.txt evidence/build.log; then
  echo 'UNEXPECTED: fake token leaked into evidence' >&2
  exit 1
else
  echo 'fake-build-token-not-found-in-metadata-history-buildlog' | tee evidence/build-secret-check.txt
fi

This proves several surfaces at once. It does not mathematically prove every cache/export/backend is clean, which is why production systems add policy, provenance, access controls, and controlled cache scopes.

5. Runtime secret: Compose grants a file, not an environment variable

cat > compose.yaml <<'EOF'
name: da-ch28
services:
  app:
    image: da-ch28:lab
    read_only: true
    tmpfs:
      - /tmp:size=8m,mode=1777
    secrets:
      - runtime_token
secrets:
  runtime_token:
    file: ./provider-store/runtime-token.txt
EOF

docker compose config > evidence/compose-normalized.yaml
docker compose up -d
APP_ID="$(docker compose ps -q app)"
docker inspect "$APP_ID" > evidence/container-inspect-v1.json
docker compose logs --no-color --tail 20 app | tee evidence/app-v1.log

The application reports only that a non-empty secret file exists. It never emits its contents. Because local Compose supplies this secret as a file mount, the value does not need to be placed in Config.Env.

docker inspect "$APP_ID" --format 'Env={{json .Config.Env}} Mounts={{json .Mounts}}'
# Expected: ordinary image environment plus a secret-file mount; no LAB-RUNTIME token value.

6. Simulate external-provider rotation

A real provider might rotate a versioned secret after policy approval. Here the provider boundary is a local file so the exercise remains free and offline. Update only the provider source, record its non-secret version, and deliberately reconcile the consumer.

printf '%s
' 'LAB-RUNTIME-TOKEN-v2-not-real' > provider-store/runtime-token.txt
printf '%s
' 'runtime-token-version=v2' > evidence/provider-version-v2.txt

docker compose up -d --force-recreate app
APP_ID_V2="$(docker compose ps -q app)"
docker inspect "$APP_ID_V2" > evidence/container-inspect-v2.json
docker compose logs --no-color --tail 20 app | tee evidence/app-v2.log

Do not generalize --force-recreate into a production rotation recipe. External managers may support agents, sidecars, SDK refresh, short leases, or mounted-file updates. The point is to document the mechanism you actually operate.

7. Challenge: identify the owning layer

A developer proposes docker run -e DB_PASSWORD=... because the app cannot start without a password. Which layer should change?

Expected reasoning: the image may remain unchanged. The runtime secret-delivery contract should change: grant a file/provider to the application, adapt the application to read it, and record which service is authorized. Do not move the same secret into Dockerfile ENV or build ARG.

8. Exact cleanup

docker compose down
docker image rm da-ch28:lab 2>/dev/null || true
cd ..
# Review evidence first, then remove only this disposable directory when no longer needed.
# rm -rf "$LAB"

No prune command is required. Keep the evidence packet if you want to review the experiment, but remember that even metadata can contain host paths/usernames and should be reviewed before sharing.

Knowledge check

Why can a secret file be outside the Docker build context and still be used with --secret?

What should docker image inspect prove after the lab build?

Why recreate the container after rotating a local Compose secret source?

What is a safe application logging rule for credentials?

A service needs a database password only at runtime. Which layer owns the problem?

Next lesson

Next: Secrets and Configuration Patterns, Runtime Injection, Environment Risk, Build Secrets, and External Secret Stores: Configuration, Design Choices, and Tradeoffs

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Version/platform baseline, verified 2026-09-22.

Docker Engine 29.8.1 is the current Engine release; Buildx 0.37.1 and BuildKit 0.33.0 are the current upstream baselines used for feature notes; Compose v5.5.1 is the current Compose release. Docker documents build arguments and Dockerfile environment variables as inappropriate for secrets because sensitive values can persist in image metadata/history, while BuildKit secret/SSH mounts expose credentials only to the build step. Compose secrets are granted per service and delivered as files (on Linux, local Compose uses a single-file bind mount); that is not the same storage/trust model as Swarm secrets or an external secret manager. Labs therefore record the actually installed Engine, Buildx, BuildKit, Compose, context, platform, and secret-delivery path before drawing conclusions.

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.