Registries, Docker Hub, Private Registries, Authentication, Credential Stores, Push/Pull, and Distribution: Guided Hands-On Workflow and Core Operations
Build a disposable loopback registry lab, publish a synthetic image, preserve the returned digest, remove local references, pull the same content by digest, inspect registry evidence, and practice credential handling without exposing a real secret.
Learning objectives
- Start an exact, labeled Distribution Registry 3.1.1 container bound only to loopback and record its image/runtime identity.
- Build, tag, push, and record a synthetic image digest; then remove local references and pull the exact content by digest.
- Use registry API/log evidence to distinguish manifest/tag state from blob transfer/reuse and local image state.
-
Demonstrate
--password-stdinplus login/logout with fake credentials in an isolated Docker client configuration without printing auth data. - Clean only the exact Chapter 14 registry/image/volume resources and retain evidence for review.
3.1.1,
and carries a Chapter 14 label. The HTTP endpoint is a development
simulation, not a production TLS pattern. Do not add a broad daemon
insecure-registries rule just to make the lab work.
1. Preflight: prove the host, context, tools, port, and image assumptions
First preserve the environment that will own every later object. The commands do not change daemon configuration.
set -eu
LAB="$HOME/devops-academy-ch14"
EVIDENCE="$LAB/evidence"
REGISTRY="127.0.0.1:5000"
REPO="devops-academy/ch14-app"
REGISTRY_CONTAINER="da-ch14-registry"
REGISTRY_VOLUME="da-ch14-registry-data"
mkdir -p "$EVIDENCE" "$LAB/app"
docker version | tee "$EVIDENCE/docker-version.txt"
docker info | tee "$EVIDENCE/docker-info.txt"
docker context show | tee "$EVIDENCE/context.txt"
docker buildx version | tee "$EVIDENCE/buildx-version.txt"
docker compose version | tee "$EVIDENCE/compose-version.txt"
docker ps -a --filter name="^/${REGISTRY_CONTAINER}$"
docker volume ls --filter name="^${REGISTRY_VOLUME}$"
If those names already exist and are not your lab resources, stop.
Do not remove them. Port 5000 must also be available on
loopback.
2. Resolve the registry software before starting it
The current stable Distribution release is 3.1.1. Pull the human-readable release tag, then preserve the repository-qualified digest actually obtained by your Engine.
docker pull registry:3.1.1 | tee "$EVIDENCE/registry-pull.txt"
docker image inspect registry:3.1.1 \
--format 'ID={{.Id}} RepoDigests={{json .RepoDigests}}' \
| tee "$EVIDENCE/registry-image.txt"
3. Start one loopback-only registry with an exact storage boundary
docker volume create \
--label devops-academy.lab=ch14 \
"$REGISTRY_VOLUME"
docker run -d \
--name "$REGISTRY_CONTAINER" \
--label devops-academy.lab=ch14 \
-p 127.0.0.1:5000:5000 \
-v "$REGISTRY_VOLUME:/var/lib/registry" \
registry:3.1.1
docker inspect "$REGISTRY_CONTAINER" \
--format 'ID={{.Id}} State={{.State.Status}} Ports={{json .NetworkSettings.Ports}} Mounts={{json .Mounts}}' \
| tee "$EVIDENCE/registry-container.txt"
docker logs "$REGISTRY_CONTAINER" 2>&1 | tail -n 40 | tee "$EVIDENCE/registry-startup.log"
-p 127.0.0.1:5000:5000 publishes only to loopback. The
named volume makes registry data lifecycle explicit. No production
registry, firewall, or daemon trust configuration is touched.
4. Build one synthetic image whose bytes are easy to verify
cat > "$LAB/app/message.txt" <<'EOF'
chapter=14
release=1.0.0
source=synthetic-local-lab
EOF
cat > "$LAB/app/Dockerfile" <<'EOF'
# syntax=docker/dockerfile:1
FROM busybox:1.37.0
LABEL devops-academy.lab="ch14"
WORKDIR /opt/ch14
COPY message.txt ./message.txt
CMD ["cat", "/opt/ch14/message.txt"]
EOF
docker buildx build --load --progress=plain \
-t da-ch14-app:1.0.0 "$LAB/app" \
2>&1 | tee "$EVIDENCE/build.log"
docker image inspect da-ch14-app:1.0.0 \
--format 'ID={{.Id}} RepoDigests={{json .RepoDigests}}' \
| tee "$EVIDENCE/local-image-before-push.txt"
Before the first push, RepoDigests can be empty for
this local build. That is expected: the image has not yet been
associated with a registry repository digest.
5. Tag for the repository, push, and capture the registry digest
REF="$REGISTRY/$REPO:1.0.0"
docker image tag da-ch14-app:1.0.0 "$REF"
docker push "$REF" 2>&1 | tee "$EVIDENCE/push-1.0.0.log"
DIGEST=$(grep -Eo 'sha256:[0-9a-f]{64}' "$EVIDENCE/push-1.0.0.log" | tail -n 1)
test -n "$DIGEST"
printf '%s\n' "$DIGEST" | tee "$EVIDENCE/registry-digest.txt"
docker image inspect "$REF" \
--format 'ID={{.Id}} RepoDigests={{json .RepoDigests}}' \
| tee "$EVIDENCE/local-image-after-push.txt"
The digest line is release evidence. The upload log may also show reused layers. Reuse means the registry already had blobs with matching content digests; it does not weaken the manifest identity.
6. Optional read-only API proof: ask the registry what the tag resolves to
If curl is available, use a HEAD request. This does not
download the manifest body.
curl -fsSI \
-H 'Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"http://$REGISTRY/v2/$REPO/manifests/1.0.0" \
| tee "$EVIDENCE/manifest-head.txt"
grep -i '^docker-content-digest:' "$EVIDENCE/manifest-head.txt" || true
The Docker-Content-Digest response should agree with
the manifest digest recorded from the push for this single-platform
lab. If it does not, preserve both values and inspect media
type/platform before guessing.
7. Remove only the disposable local references, then pull by digest
docker image rm "$REF" da-ch14-app:1.0.0
# The registry remains. Rehydrate by immutable registry identity.
PINNED="$REGISTRY/$REPO@$DIGEST"
docker pull "$PINNED" 2>&1 | tee "$EVIDENCE/pull-by-digest.log"
docker image inspect "$PINNED" \
--format 'ID={{.Id}} RepoDigests={{json .RepoDigests}} Architecture={{.Architecture}} OS={{.Os}}' \
| tee "$EVIDENCE/pulled-image.txt"
docker run --rm "$PINNED" | tee "$EVIDENCE/runtime-output.txt"
This proves the consumer can ask for the exact registry object even after its original local aliases are gone. Shared base blobs may still exist locally, so “pull by digest” does not necessarily mean every byte crosses the network again.
8. Credential handling demo with fake data and isolated client configuration
The lab registry does not enforce authorization, so this step demonstrates client credential handling only. It does not pretend that a no-auth registry is protected.
OLD_DOCKER_CONFIG="${DOCKER_CONFIG:-}"
export DOCKER_CONFIG="$LAB/docker-config"
mkdir -p "$DOCKER_CONFIG"
printf '%s\n' 'fake-local-password' \
| docker login "$REGISTRY" --username localstudent --password-stdin \
| tee "$EVIDENCE/login-result.txt"
# Prove a client config exists without printing any auth material.
test -s "$DOCKER_CONFIG/config.json" && echo 'isolated Docker config created'
grep -E '"credsStore"|"credHelpers"' "$DOCKER_CONFIG/config.json" || true
docker logout "$REGISTRY" | tee "$EVIDENCE/logout-result.txt"
unset DOCKER_CONFIG
if [ -n "$OLD_DOCKER_CONFIG" ]; then export DOCKER_CONFIG="$OLD_DOCKER_CONFIG"; fi
Real accounts should use the platform's native keychain or a configured helper and short-lived/revocable credentials. Never check a populated Docker config or token into source control.
9. Registry evidence: inspect logs without dumping client auth material
docker logs "$REGISTRY_CONTAINER" 2>&1 \
| tail -n 120 \
| tee "$EVIDENCE/registry-requests.log"
docker inspect "$REGISTRY_CONTAINER" \
--format 'State={{.State.Status}} Started={{.State.StartedAt}} RestartCount={{.RestartCount}}' \
| tee "$EVIDENCE/registry-final-state.txt"
Do not enable indiscriminate HTTP debug tracing against real authenticated registries because Authorization headers and tokens can become incident artifacts themselves.
10. Challenge: choose the causal layer
A teammate says: “The push succeeded, but another host still runs the old release. Should we restart the registry?” Before changing anything, classify the evidence you need: the pushed digest, the mutable tag's current registry resolution, the consumer's image reference/pull policy, the consumer's local RepoDigest, and the container image actually created. A registry restart is unrelated unless evidence shows the registry service itself is failing.
11. Exact cleanup: remove only Chapter 14 resources
# Guard exact lab ownership before deleting.
[ "$(docker inspect -f '{{ index .Config.Labels "devops-academy.lab" }}' "$REGISTRY_CONTAINER")" = 'ch14' ]
docker container rm -f "$REGISTRY_CONTAINER"
docker volume rm "$REGISTRY_VOLUME"
# Remove only lab references if present; do not prune unrelated content.
docker image rm "$REGISTRY/$REPO@$DIGEST" 2>/dev/null || true
docker image rm "$REGISTRY/$REPO:1.0.0" da-ch14-app:1.0.0 2>/dev/null || true
# Keep $EVIDENCE until you finish reviewing it.
docker ps -a --filter label=devops-academy.lab=ch14
docker volume ls --filter label=devops-academy.lab=ch14
Knowledge check
After docker push, why remove both local aliases
before pulling by digest?
It makes the retrieval test independent of the original aliases and local image object. Shared blobs can still be cached, but the registry identity must resolve again.
Why bind the lab registry to 127.0.0.1 rather than
0.0.0.0?
It prevents accidentally exposing the unauthenticated HTTP development registry to other hosts.
What does “Layer already exists” prove during a push?
The registry already has a blob with that content digest. It is reuse evidence, not a failure and not proof that the manifest/tag already existed.
Why use an isolated DOCKER_CONFIG for the fake
login demo?
It prevents the exercise from modifying or exposing the learner’s real registry credentials/helper configuration.
A push succeeded but another host still runs the old image. Which layer do you inspect first?
Compare pushed digest/tag resolution with the consumer’s pull policy/reference, local RepoDigest, and container image. Do not assume the registry needs restarting.
Official references and version notes
-
docker login— authentication,--password-stdin, Docker Desktop native keychains,credsStore, and per-registrycredHelpers. -
docker image push— repository naming, layer reuse, push progress, and digest output. -
docker image pull— pull-by-tag versus pull-by-digest and registry-qualified references. - Registry certificates — platform-specific CA/client-certificate configuration and TLS verification.
- Docker daemon: insecure registries — why plaintext/untrusted registries are testing-only and why trusted CA configuration is preferred.
- Docker Hub pull usage and limits — current rate-limit behavior and authentication attribution.
- CNCF Distribution Registry — current Registry v3 documentation and local-registry workflow.
-
Registry v2 token authentication
—
401challenge, token service, repository scope, Bearer token, and retry flow. - Distribution HTTP API V2 — manifests, blobs, uploads, errors, and digest-addressed operations.
- Deploying Distribution Registry — local development registry, production TLS/auth expectations, and storage.
- Registry garbage collection — shared blobs, reference-aware deletion, mark/sweep behavior, and read-only guidance.
- OCI Distribution Specification v1.1.1 — latest OCI Distribution Specification release used as the standards baseline.
- Distribution Registry v3.1.1 — current stable registry release used as the local-lab baseline.
- Docker Engine 29 release notes — Engine 29.8.1 baseline used when authoring this chapter.
- Buildx releases, BuildKit releases, and Compose releases — current build/orchestration tool release history.
Version-sensitive statements were rechecked against primary documentation on 2026-09-21. The authoring baseline is Docker Engine 29.8.1, Buildx 0.37.1, BuildKit 0.33.0, Dockerfile frontend 1.27.0, Compose 5.5.1, CNCF Distribution Registry 3.1.1, and OCI Distribution Specification 1.1.1. The executable labs still record the versions, context, image digests, registry endpoint, and credential/TLS assumptions actually present. Docker Hub limits and hosted-registry policies are service policy and can change independently of the Docker Engine.
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.