Registries and Image Distribution

Tagging for a registry, pushing to Docker Hub, ECR and ACR, why a tag is not an identity, and the rate limits and retention rules that break builds at the worst time.

intermediate 18 min lesson hands-on task included

An image on your laptop is not a deliverable. A registry is the storage and distribution service that turns a local build into something CI, staging and production can all pull — and the tagging discipline you apply there is what makes a rollback possible.


Topic 1: Push, Pull and What a Repository Name Means

Two verbs:

  • Pull — download an image from a registry into the local image store.
  • Push — upload a local image to a registry.

The catch is that an image’s name determines where it pushes. A full image reference looks like:

registry-host[:port]/namespace/repository:tag

When the registry host is omitted, Docker assumes Docker Hub — which is why nginx:alpine really means docker.io/library/nginx:alpine. That default is the reason your first docker push myapp:1.0 fails: it is trying to push to docker.io/library/myapp, a namespace you do not own.

The fix is to tag the image with a name that matches your repository:

docker build -t gameoflife:1.0 .
docker tag gameoflife:1.0 tmaheedhar2/gameoflife:1.0
docker login
docker push tmaheedhar2/gameoflife:1.0

docker tag does not copy anything. It adds a second name pointing at the same image ID — cheap, instant, and the reason you can carry several tags on one build.

docker images
# REPOSITORY                  TAG    IMAGE ID       SIZE
# gameoflife                  1.0    a1b2c3d4e5f6   190MB
# tmaheedhar2/gameoflife      1.0    a1b2c3d4e5f6   190MB   ← same ID

Topic 2: The Registries You Will Meet

Docker Hub is the default and the largest public registry. Free public repositories, paid private ones. Two things to know before depending on it in a pipeline:

  • Anonymous pull rate limits are real and they are counted per source IP. A CI fleet behind one NAT gateway hits them quickly, and the failure looks like toomanyrequests: You have reached your pull rate limit. Authenticate your builds, or mirror the images you depend on.
  • Official images (nginx, postgres, python) live in the library namespace and are maintained by Docker and upstream projects. Anything else on Hub is maintained by whoever pushed it, which is worth remembering before you base production on a stranger’s image.

AWS Elastic Container Registry (ECR) — private by default, IAM-controlled, and the natural choice on AWS:

aws ecr get-login-password --region us-east-1 \
  | docker login --username AWS --password-stdin 296760598094.dkr.ecr.us-east-1.amazonaws.com

docker tag gameoflife:1.0 296760598094.dkr.ecr.us-east-1.amazonaws.com/gameoflife:1.0
docker push 296760598094.dkr.ecr.us-east-1.amazonaws.com/gameoflife:1.0

The login token expires after 12 hours, so this is a step in the pipeline rather than a one-off setup. Note also that ECR repositories must exist before you push — unlike Docker Hub, it will not create one for you.

Azure Container Registry (ACR):

az login
az acr login --name myregistry
docker tag gameoflife:1.0 myregistry.azurecr.io/gameoflife:1.0
docker push myregistry.azurecr.io/gameoflife:1.0

Others you will encounter: Google Artifact Registry, GitHub Container Registry (ghcr.io, convenient when your code is already on GitHub), JFrog Artifactory and Nexus (usually where an enterprise already keeps its other artefacts, with LDAP integration and retention policy already solved).

A registry you run yourself is a container:

docker run -d -p 5000:5000 --restart always --name registry registry:2
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0

Useful for air-gapped environments and for local experiments. In production it needs TLS, authentication and storage backing — the plain registry:2 above is fine on a laptop and not fine on a network.


Topic 3: Tagging Strategy

This is the part that determines whether an incident is a two-minute rollback or an archaeology exercise.

A tag is a mutable pointer. myapp:1.0 can be re-pushed tomorrow to point at different bytes and nothing will warn you. A digest is immutable — it is the SHA-256 of the image manifest:

docker images --digests
docker pull myapp@sha256:c5a1f5a3b8e2...        # exactly these bytes, always

A tagging scheme that works:

TagMutable?Purpose
1.4.2Never re-pushedThe release. What you roll back to
a3f9c21 (git SHA)Never re-pushedTraces the image to the exact commit
staging, prodMovesA pointer to whatever is currently deployed
latestMovesConvenience for humans. Never for deployment
GIT_SHA=$(git rev-parse --short HEAD)
docker build -t acme/api:1.4.2 -t acme/api:$GIT_SHA -t acme/api:staging .
docker push --all-tags acme/api

The rules that make this pay off:

  • Never re-push an immutable tag. If 1.4.2 is wrong, 1.4.3 fixes it. Re-pushing means two hosts running “1.4.2” can be running different code, and you will lose an afternoon to it.
  • Never deploy :latest. It is a tag like any other, it moves without notice, and it silently sets imagePullPolicy: Always in Kubernetes. More importantly there is nothing to roll back to.
  • Deploy by digest where it matters. A tag says which release you meant; a digest says which bytes you got.
  • Turn on tag immutability if your registry supports it (ECR and most enterprise registries do). Let the registry enforce the rule rather than your team’s discipline.

Topic 4: The Build → Push → Deploy Workflow

The Dockerfile belongs in the application repository, next to the code it builds. That single decision gives you a versioned build definition that changes in the same commit as the code it packages.

The pipeline shape, whatever CI system you use:

  1. Commit lands on a branch.
  2. CI checks out the code and builds the image, tagging with the semantic version and the git SHA.
  3. CI runs tests — either against the built image, or in a --target test stage of the same build (lesson 7).
  4. CI scans the image for vulnerabilities and fails on findings above your threshold (lesson 12).
  5. CI authenticates to the registry and pushes.
  6. Deployment references the image by version or digest.
docker build -t acme/api:$VERSION -t acme/api:$GIT_SHA .
docker run --rm acme/api:$VERSION /app/run-tests.sh
docker scout cves acme/api:$VERSION            # or trivy image acme/api:$VERSION
docker login -u "$REGISTRY_USER" --password-stdin <<< "$REGISTRY_TOKEN"
docker push --all-tags acme/api

docker login reading from stdin is the correct form. Passing -p "$TOKEN" on the command line puts the credential in the process list and your shell history.

Where credentials actually live:

docker login writes to ~/.docker/config.json, and by default it stores the credential base64-encoded, not encrypted. On a shared or persistent build host that is a real exposure. Use a credential helper (docker-credential-ecr-login, docker-credential-osxkeychain, docker-credential-secretservice), and prefer short-lived tokens over long-lived passwords.


Topic 5: Getting Images Around Without a Registry

Occasionally you need to move an image and there is no registry — an air-gapped environment, a customer site, a debugging session on a disconnected host.

docker save -o api-1.4.2.tar acme/api:1.4.2       # image → tar, keeps layers and tags
docker load -i api-1.4.2.tar                       # tar → image store

Do not confuse these with the container-level pair:

docker export mycontainer > fs.tar     # a container's FLATTENED filesystem — no layers, no history, no CMD
docker import fs.tar myimage:1.0       # tar → a single-layer image

save/load preserves the image with its layers and metadata. export/import flattens everything and throws away the configuration, which means the resulting image has no entrypoint and no history. export is for filesystem forensics, not for distribution.

docker commit belongs in the same footnote: it turns a modified container into an image. It works, and it produces an artefact nobody can reproduce or review. Use it to snapshot a broken container for offline debugging; never as a way to build.

Try it yourself: Run docker pull nginx:alpine twice on a clean host and note the layer download messages. Now docker pull nginx:1.27-alpine — most layers report “Already exists” because they are shared with the tag you already have. That is layer sharing from lesson 5, now visible over the network, and it is why registries are cheap to pull from.

Common mistake: Deploying a moving tag and then trying to roll back. If production was running acme/api:staging and staging has moved on twice since, there is no command that tells you which bytes were running an hour ago. Deploy immutable references — a version tag you never re-push, or a digest — and the rollback becomes a one-line change.