CertGrid CertGrid
Hands-on Lab·Docker

Docker Image Tagging and Publishing

How an image name is actually parsed, why :latest is a trap, and a full push and pull against a registry running in a container on your own machine - no account, no credentials, real digests.

Dockerfiles and Builds Guide 20 of 46 Intermediate

Tested on the versions above. Digests, layer IDs and manifest sizes differ for every image you build. Match the shape of the output, not the exact strings.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. How an image name is parsed

    A full reference is registry/namespace/repository:tag. When you omit parts, Docker fills them in: alpine:3.22 becomes docker.io/library/alpine:3.22. The rule that surprises people is how the registry is detected - a first segment containing a dot or a colon is treated as a hostname. That is why localhost:5000/name targets your local registry while myteam/name targets Docker Hub.

    bash
    docker pull alpine:3.22Status: Downloaded newer image for alpine:3.22docker.io/library/alpine:3.22

    Expected resultThe last line shows the fully qualified name Docker resolved.

    Success conditionYou can see the implicit docker.io/library/ prefix that shorthand hides.

  2. Run a registry you control

    You do not need an account to learn this. The registry is itself a container. Running one locally means every push and pull below is real, and nothing leaves your machine.

    bash Example session
    docker run -d --name cg-registry -p 5000:5000 registry:3Status: Downloaded newer image for registry:388b627095a6001f69ff1f826e3ccc65550c77debe5cbd6076d350bc1715ff0adcurl -s http://localhost:5000/v2/_catalog{"repositories":[]}

    Expected resultA container ID, then an empty catalogue.

    Success conditionThe catalogue responds with empty repositories. If curl fails, the registry has not finished starting - the same readiness point as guide 5.

  3. Tag for the destination registry

    Pushing does not take a destination argument. The destination is encoded in the image name, so publishing always means tagging first. The tag adds a name - it does not copy the image.

    bash
    docker tag cg-multi:1 localhost:5000/cg-multi:1# succeeds silently

    Expected resultNo output.

    Success conditiondocker images shows both names pointing at the same image ID.

  4. Push it

    Each layer is uploaded once and reported individually. Layers the registry already holds are skipped, which is why the second push of a similar image is much faster. The final line is the manifest digest - the immutable identity of exactly this image.

    bash Example session
    docker push localhost:5000/cg-multi:1The push refers to repository [localhost:5000/cg-multi]44136fa355b3: Pushed33332d5fcf3a: Pushedbc3d926f773a: Pushed1: digest: sha256:e2afd891162845f0908563e47ab6194b57d3ed8bb7ea7eacd83d6be5b6680720 size: 855

    Expected resultOne line per layer and a digest at the end.

    Success conditionA digest is returned. Record it - that is what you pin in a deployment when you want certainty about which build is running.

  5. Prove it round-trips

    Remove the local copy entirely, then pull it back. The digest matches the one from the push, which is the actual proof that what came back is byte-for-byte what went up.

    bash Example session
    curl -s http://localhost:5000/v2/_catalog{"repositories":["cg-multi"]}docker rmi localhost:5000/cg-multi:1 && docker pull localhost:5000/cg-multi:1Untagged: localhost:5000/cg-multi:1Digest: sha256:e2afd891162845f0908563e47ab6194b57d3ed8bb7ea7eacd83d6be5b6680720Status: Downloaded newer image for localhost:5000/cg-multi:1

    Expected resultThe repository now listed in the catalogue, and a pull whose digest matches the push.

    Success conditionSame digest both directions. A different digest would mean you are not looking at the same image.

  6. Why :latest is a trap

    latest is not a special tag. It is the default label Docker applies when you give none, and it has no relationship to recency - it points wherever it was last pushed. Two machines pulling app:latest a week apart can legitimately run different code, and neither will tell you. Use explicit version tags for humans and digests where correctness matters.

    bash Example session
    docker image inspect alpine:3.22 --format "{{index .RepoDigests 0}}"alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce# pin that instead of a tag when the build must be reproducible

    Expected resultA name@sha256:... reference.

    Success conditionYou have a digest reference. Deploying by digest removes an entire class of "works on my machine" problem.

  7. Clean up

    Remove the registry container and the images this guide tagged. The registry stores its data in an anonymous volume, so remove that too or it lingers unnamed.

    bash
    # removes the registry and everything pushed to itdocker rm -f cg-registrydocker rmi localhost:5000/cg-multi:1 registry:3# check for the anonymous volume the registry created: docker volume ls

    Expected resultThe container name, then untagged lines.

    Success conditiondocker ps -a no longer lists cg-registry. Check docker volume ls for the leftover anonymous volume and remove it by name.

Troubleshooting

Official sources