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
- OSUbuntu 26.04 LTS (resolute)
- Docker Engine29.7.2
- Shellbash
- Architectureamd64
- TimeAbout 14 min
- Reviewed21 August 2026
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.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| DOCKER01 | 192.168.0.21 | Ubuntu 26.04 LTS | Docker Host | 2 Core | 4 GB | 50 GB |
Before you start
- An image you have built - guide 14.
- Host port 5000 free for the local registry.
- Understanding of tags versus digests - guide 2.
-
How an image name is parsed
A full reference is
registry/namespace/repository:tag. When you omit parts, Docker fills them in:alpine:3.22becomesdocker.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 whylocalhost:5000/nametargets your local registry whilemyteam/nametargets Docker Hub.bash docker pull alpine:3.22Status: Downloaded newer image for alpine:3.22docker.io/library/alpine:3.22Expected resultThe last line shows the fully qualified name Docker resolved.
Success conditionYou can see the implicit
docker.io/library/prefix that shorthand hides. -
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.
-
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 silentlyExpected resultNo output.
Success condition
docker imagesshows both names pointing at the same image ID. -
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: 855Expected 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.
-
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:1Expected 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.
-
Why :latest is a trap
latestis 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 pullingapp:latesta 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 reproducibleExpected resultA name@sha256:... reference.
Success conditionYou have a digest reference. Deploying by digest removes an entire class of "works on my machine" problem.
-
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 lsExpected resultThe container name, then untagged lines.
Success condition
docker ps -ano longer lists cg-registry. Checkdocker volume lsfor the leftover anonymous volume and remove it by name.
Troubleshooting
http: server gave HTTP response to HTTPS client
Why: Docker requires TLS for registries by default. A plain-HTTP local registry is refused unless the daemon is told to allow it.
Fix:
localhost:5000is treated as insecure-by-default on most setups, which is why this guide works unmodified. For a registry on another host you must either give it a certificate or add it to insecure-registries in daemon.json - a daemon change, so understand the exposure first.bash # editing daemon.json restarts the daemon and affects every container on the hostdocker info --format "{{.RegistryConfig.InsecureRegistryCIDRs}}"denied: requested access to the resource is denied
Why: The repository name does not match an account you can write to, or you are not logged in. Pushing
myimage:1sends it to Docker Hub's official namespace, which you do not own.Fix:Tag with the full destination including your namespace, and authenticate first.
bash docker tag myimage:1 myusername/myimage:1docker loginThe push says Layer already exists and finishes instantly
Why: Not an error. The registry already holds those layers, usually from a previous push of an image sharing the same base.
Fix:Nothing to fix - this is layer deduplication working, and it is why keeping a common base across your images makes deployments faster.