Registries, tags & sharing images
push, pull, and how tags work.
You have never compiled the apps on your phone. You open a store, search, tap install, and finished software lands on your device because somebody else published it there. Docker moves images the same way. An image is a read-only bundle of an app plus everything it needs to run, like a frozen meal with the food and every ingredient sealed into one box you cannot change once it leaves the factory. A registry is the store those boxes live in. Publishers upload images to it (an upload is called a push), and anyone with access downloads them (a download is a pull). The best-known store is Docker Hub, and Docker looks there unless you tell it otherwise. That is the whole reason docker run nginx works on a machine that has never seen nginx before. Docker quietly pulls the image from Docker Hub first, then starts it.
Downloading and running are separate steps, so you can pull an image and not run it. A pull fetches the image and parks it on your disk, ready for whenever you want it. Grab a specific version of nginx, a free web server that a large slice of the internet runs on.
$ docker pull nginx:1.27-alpine
1.27-alpine: Pulling from library/nginx9c1b6dd6c1e6: Pull complete2c7498eef94a: Pull completeb3f4118c8d55: Pull completeDigest: sha256:2140dad235c1a3f9e12a...Status: Downloaded newer image for nginx:1.27-alpinedocker.io/library/nginx:1.27-alpine
What an image name is actually telling you
Look at the last line of that output: docker.io/library/nginx:1.27-alpine. You typed nginx:1.27-alpine, and Docker filled in the rest for you. A full image name reads like a postal address: registry / namespace / name, with a version label after the colon. The registry is the street address of the store, and docker.io is Docker Hub. The namespace is the account that owns the image, and library is Docker's own account, the one it uses for official images. The name is the image itself. The tag is the version label, like the edition printed inside a book. Same title, different printing. Leave the registry off and Docker assumes docker.io. Leave the namespace off on an official image and it assumes library. So plain nginx really means docker.io/library/nginx:latest, spelled short.
Getting your own image onto the shelf
Pulling other people's work is half of the story. The other half is publishing your own. Say you built an image on your laptop and called it payments-api:1.0. That name carries no registry and no account, so Docker has no idea where to send it. Nobody addressed the envelope. docker tag fixes that by hanging a second name on the image, one that spells out a destination. Tagging copies nothing and rebuilds nothing. It sticks another label on bytes you already have.
$ docker tag payments-api:1.0 sachinchr/payments-api:1.0$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZEpayments-api 1.0 9c6f07244728 3 minutes ago 181MBsachinchr/payments-api 1.0 9c6f07244728 3 minutes ago 181MB
Two rows, one IMAGE ID. That column is Docker's internal reference for the image data itself, and tagging never touches it, so the matching ID proves you have a single image wearing two names. The new name says the image belongs to the sachinchr account on Docker Hub. Now you can push it. Try that before logging in, though, and here is what comes back.
$ docker push sachinchr/payments-api:1.0
The push refers to repository [docker.io/sachinchr/payments-api]5f70bf18a086: Preparinga83c2c542782: Preparingdenied: requested access to the resource is denied
Read the bottom line. "denied: requested access to the resource is denied" means the registry does not believe you are allowed to write to that account. Two causes cover nearly every case: you never logged in, or you are pushing into a namespace that belongs to somebody else. Log in as the account that owns the name, then push again.
$ docker loginUsername: sachinchrPassword:Login Succeeded$ docker push sachinchr/payments-api:1.0
The push refers to repository [docker.io/sachinchr/payments-api]5f70bf18a086: Pusheda83c2c542782: Pushedd310e774110a: Layer already exists1.0: digest: sha256:7d3e0e6f1c9b... size: 1789
That went through. Now look at the line reading "Layer already exists". Images are built in stacked sheets called layers, and the image is the whole stack. The registry already held that one sheet from some other image, so Docker skipped sending it a second time. Shared layers are why your second and third pushes finish so much faster than the first.
From any other machine, anyone with access pulls the image back using that exact name. They need no copy of your laptop, your source code or your build files. The registry hands them a finished, ready-to-run copy.
$ docker pull sachinchr/payments-api:1.0
1.0: Pulling from sachinchr/payments-apid310e774110a: Already existsa83c2c542782: Pull complete5f70bf18a086: Pull completeDigest: sha256:7d3e0e6f1c9b...Status: Downloaded newer image for sachinchr/payments-api:1.0docker.io/sachinchr/payments-api:1.0
Docker Hub is not the only store in town. Companies run private registries for code they would rather keep off the public internet: one from their cloud provider, or the registry built into GitLab or GitHub. The commands do not change. You log in to that registry, put its address at the front of the tag (registry.example.com/team/app:1.0), then push and pull the way you did above. Privacy has a hard practical edge here. Anyone who can pull an image can read everything baked into its layers, so passwords, keys and private source code have no business inside an image pushed to a public account.
Why ':latest' catches people out
One tag confuses beginners more than all the others put together: latest. Push or pull a name with no tag and Docker fills in :latest for you. It sounds like a promise about time. It is nothing of the kind. :latest is the default label, and it points at whichever image was pushed to it most recently, which could easily be a build from six months ago that nobody has touched since. No background process moves it on your behalf. Give every build its own honest tag, something like 1.4.2, and you always know exactly what you are pointing at.
Strip all of this back and a registry is a shared shelf. Your laptop builds an image, and until that image goes somewhere else, it exists on exactly one hard drive in the world. push puts it on the shelf. pull takes a copy down. Every deployment pipeline you will ever meet rests on that handoff: one machine builds and pushes, other machines pull and run, and none of them need to know a thing about each other.
The name and the fingerprint do different jobs. sachinchr/payments-api is the repository, the slot on the shelf. The tag after the colon is a label you can peel off and stick on a different image tomorrow. The digest is calculated from the image content itself, so one digest can only ever mean one exact set of bytes. Change a single byte, get a different digest. While you are learning, tags are easier to read and perfectly fine. Deployment pipelines lean on digests instead, or on registry rules that forbid overwriting a tag once it has been published.
docker login saves a credential on the machine so later pushes and pulls to private repositories go through without asking again. Guard that credential the way you guard a house key. The same care goes for build machines. Whoever can push to the tag your production servers pull, say :prod, decides what production runs the next time it restarts, and they never have to touch production to do it.
You already have the receipt for a clean transfer. The push printed digest: sha256:7d3e0e6f1c9b..., and the pull on the far machine printed Digest: sha256:7d3e0e6f1c9b... too. Matching strings mean the bytes arrived intact and nobody moved the name onto a different image in between. Build locally under whatever name suits you, tag it with the account and version you want published, push, then compare the two digests.
When a pull fails inside CI (continuous integration, the service that builds and tests your code automatically on a server somewhere), the Dockerfile is rarely the culprit. Public registries cap how many images an anonymous machine may download per hour, and CI runners are very often anonymous. Private networks block outbound traffic. Names collect typos. Check those three before you start editing build instructions that worked fine yesterday.
Try this
Here is a two-minute exercise that makes the pointer idea concrete. Pull a tiny image, print its internal ID along with the names attached to it, then hang a second name on it and watch both names report the same image.
docker pull alpine:3.20docker image inspect alpine:3.20 --format "{{.Id}} {{.RepoTags}}"docker tag alpine:3.20 local-alpine:coursedocker images local-alpine
3.20: Pulling from library/alpine...sha256:........ [alpine:3.20]REPOSITORY TAG IMAGE ID CREATED SIZElocal-alpine course ........ weeks ago 7.33MB
Takeaway
Names are pointers; image data is not. docker tag moves a pointer, push and pull move the actual bytes, and :latest is the pointer most likely to shift behind your back while you sleep.