Chapter 14Lesson 02~125 minutes

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.

Local registryPush / pullDigest evidenceCredential handlingCleanup

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-stdin plus 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.
Lab scope. Everything below is disposable and local. The registry binds only to loopback, uses Registry 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
Next lesson

Next: Configuration, Design Choices, and Tradeoffs

Choose registry hosting, credential storage, TLS, visibility, and retention patterns by observable trust and operations prerequisites.

Knowledge check

After docker push, why remove both local aliases before pulling by digest?

Why bind the lab registry to 127.0.0.1 rather than 0.0.0.0?

What does “Layer already exists” prove during a push?

Why use an isolated DOCKER_CONFIG for the fake login demo?

A push succeeded but another host still runs the old image. Which layer do you inspect first?

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.