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.
Learning objectives
- Create a disposable project that keeps fake credentials out of the build context and final image.
-
Use BuildKit
--secretwith 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
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?
The build client supplies the explicitly named secret through the BuildKit session; it is not obtained by COPY from the build context. This is precisely why the secret can be excluded by .dockerignore.
What should docker image inspect prove after the lab build?
The final image configuration should not contain the fake secret in Config.Env or other configured metadata, while the image can still contain non-secret build outputs that prove the protected step completed.
Why recreate the container after rotating a local Compose secret source?
Local Compose secrets are mounted from a source file for the container. Rotation is a lifecycle action; you should deliberately reconcile/recreate the consumer and verify the new version rather than assume every runtime secret mechanism hot-reloads automatically.
What is a safe application logging rule for credentials?
Log the fact that a credential was found/used and a non-sensitive version identifier when useful, but never the credential value, authorization header, private key, or transformed value that still carries secret entropy.
A service needs a database password only at runtime. Which layer owns the problem?
Runtime configuration/secret delivery, not the Dockerfile or image-build layer. BuildKit build secrets solve build-time authentication, not application runtime secret distribution.
Official references and version notes
- Docker Build secrets — secret mounts, SSH mounts, Git authentication secrets, file/environment sources, and build-time scope.
-
Dockerfile reference
—
RUN --mount=type=secret,RUN --mount=type=ssh,required, target, ownership, and environment exposure options. -
SecretsUsedInArgOrEnv build check
— why Dockerfile
ARG/ENVare not secret channels. - Manage secrets securely in Docker Compose — per-service grants and file-based runtime delivery.
- Compose secrets reference — top-level secret definitions and service consumption.
- Compose environment-variable best practices — configuration precedence and why sensitive data should use secrets.
- Swarm secrets — orchestrator-managed secret semantics, intentionally distinct from local Compose file mounts.
- Docker Engine 29 release notes — current Engine baseline.
- Buildx releases and BuildKit releases — current builder/frontend assumptions.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.