Registries, tags and image distribution

How an image name is assembled, why digests are the only immutable reference, and pushing to Docker Hub, GHCR or a private registry.

Anatomy of an image name

registry.example.com/team/api:1.4.2-alpine
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”¬β”€β”˜ β””β”¬β”˜ β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
     registry host     namespace repo    tag

nginx                       -> docker.io/library/nginx:latest
ghcr.io/acme/api:sha-9f3c1  -> tag carries the commit
app@sha256:8f1c...          -> digest: immutable, exact bytes
  • Omit the registry host and Docker Hub is assumed; omit the namespace and library is assumed for official images.
  • A tag is a mutable pointer that can be moved to a completely different image at any time. A digest always resolves to the same content.
  • latest is only the default tag β€” it is not the newest build, and pulling it twice can give you two different images.
  • Repository names must be lowercase; a mixed-case name fails at push time, which is the classic first push error.
docker tag app:local registry.example.com/team/app:1.4.2
docker tag app:local registry.example.com/team/app:1
docker push registry.example.com/team/app:1.4.2
docker push registry.example.com/team/app:1

# pin the exact bytes you tested
docker inspect --format='{{index .RepoDigests 0}}' registry.example.com/team/app:1.4.2
docker pull registry.example.com/team/app@sha256:8f1c9a4e...
πŸ’‘
Deploy by digest and tag alongside it: keep the human-readable tag for discovery and the digest for the deployment record. When someone asks in three months which image was running, only the digest answers unambiguously.

Logging in and pushing

RegistryHostAuth notes
Docker Hubdocker.io, plain user/imagePersonal access token; anonymous pulls are rate limited
GitHub Container Registryghcr.ioGITHUB_TOKEN in Actions, PAT elsewhere
Amazon ECR*.dkr.ecr.<region>.amazonaws.comToken from the AWS CLI, valid 12 hours
Google Artifact Registry<region>-docker.pkg.devAccess token as the password
Self-hostedYour own host nameregistry:2 with TLS and htpasswd for anything real
# never let the password reach the shell history or process list
echo "$REGISTRY_TOKEN" | docker login registry.example.com -u ci --password-stdin
docker logout registry.example.com

# GHCR push from a workstation
echo "$GHCR_PAT" | docker login ghcr.io -u "$GITHUB_USER" --password-stdin
docker tag app:local ghcr.io/acme/app:1.4.2
docker push ghcr.io/acme/app:1.4.2

# a local registry for the lab, and its catalogue
docker run -d -p 5000:5000 --name registry -v registry:/var/lib/registry registry:2
docker tag app:local localhost:5000/app:test
docker push localhost:5000/app:test
curl -s localhost:5000/v2/_catalog
  • Credentials land in ~/.docker/config.json, base64-encoded rather than encrypted. Configure a credential helper (docker-credential-desktop, pass, secretservice) for anything long-lived.
  • In CI, log in with a short-lived token scoped to push only that repository, and log out in a cleanup step.
  • A registry on plain HTTP must be listed in insecure-registries in daemon.json; doing the same thing in production gives away credentials on the wire.
  • Push the tag and the digest you tested. Rebuilding at deploy time can produce a different image from the one that passed CI.

Manifests, digests and multi-architecture tags

docker buildx imagetools inspect registry.example.com/team/app:1.4.2
docker manifest inspect alpine:3.20 | head -40

# what the tag actually points at, per platform
docker buildx imagetools inspect --raw nginx:1.27-alpine | jq '.["mediaType"]'

# force the platform you want instead of the emulated default
docker pull --platform linux/arm64 alpine:3.20
docker run --rm --platform linux/amd64 alpine:3.20 uname -m
  • One tag may point to a manifest list β€” an index of per-platform manifests β€” so docker pull picks the image matching your architecture automatically.
  • A digest identifies one platform-specific manifest, not the whole list, so --platform and @sha256: must agree.
  • Copy an image between registries without pulling it locally with docker buildx imagetools create --tag.
  • Removing a tag from a registry does not always remove the layers immediately; retention policies and garbage collection are registry features, not Docker ones.

FAQ

Why does 'docker push' fail with 'requested access to the resource is denied'?
The name does not match a repository you can write to. Check the registry host spelling, that the namespace exists and is yours, that the repository name is lowercase, and that the token has push scope rather than pull-only.
Is ':latest' ever acceptable?
For quick local experiments. Not in a Compose file, a Kubernetes manifest or a deployment script, because the same reference silently resolves to different content over time. Pin a version tag plus a digest for anything reproducible.

BuildKit, cache and multi-platform images Docker in CI and publishing images

Last refreshed 2026-09-18.