CoursesDocker for beginnersRegistries, tags & sharing images

Registries, tags & sharing images

push, pull, and how tags work.

Beginner10 min · lesson 15 of 16
In plain terms
A registry is an app store for images: publishers upload (push) their apps, and anyone can download (pull) them. Docker Hub is the default public store; companies run their own private ones for in-house apps.

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.

terminal
$ docker pull nginx:1.27-alpine
output
1.27-alpine: Pulling from library/nginx
9c1b6dd6c1e6: Pull complete
2c7498eef94a: Pull complete
b3f4118c8d55: Pull complete
Digest: sha256:2140dad235c1a3f9e12a...
Status: Downloaded newer image for nginx:1.27-alpine
docker.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.

terminal
$ docker tag payments-api:1.0 sachinchr/payments-api:1.0
$ docker image ls
output
REPOSITORY TAG IMAGE ID CREATED SIZE
payments-api 1.0 9c6f07244728 3 minutes ago 181MB
sachinchr/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.

terminal
$ docker push sachinchr/payments-api:1.0
output
The push refers to repository [docker.io/sachinchr/payments-api]
5f70bf18a086: Preparing
a83c2c542782: Preparing
denied: 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.

terminal
$ docker login
Username: sachinchr
Password:
Login Succeeded
$ docker push sachinchr/payments-api:1.0
output
The push refers to repository [docker.io/sachinchr/payments-api]
5f70bf18a086: Pushed
a83c2c542782: Pushed
d310e774110a: Layer already exists
1.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.

terminal
$ docker pull sachinchr/payments-api:1.0
output
1.0: Pulling from sachinchr/payments-api
d310e774110a: Already exists
a83c2c542782: Pull complete
5f70bf18a086: Pull complete
Digest: sha256:7d3e0e6f1c9b...
Status: Downloaded newer image for sachinchr/payments-api:1.0
docker.io/sachinchr/payments-api:1.0
How an image travels from your laptop to another machine
1docker build
image sits on your laptop
2docker tag
give it a registry address
3docker push
upload it to the registry
4docker pull
any machine downloads it
The registry sits in the middle. Push once, and every machine with access pulls back the same image.

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.

'latest' does not mean newest
Two servers can both run docker pull myapp and end up on different images, if somebody pushes a new :latest between the two pulls. On production systems (the live setup your real users hit), pull an explicit tag such as 1.4.2. Better still, pin the image to its digest (myapp@sha256:7d3e0e6f1c9b...), a fingerprint calculated from the image content that can never point anywhere else. Pinning nails the reference down so it cannot drift, and every machine runs the version you chose instead of whatever happened to be sitting under :latest that day.

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.

terminal
docker pull alpine:3.20
docker image inspect alpine:3.20 --format "{{.Id}} {{.RepoTags}}"
docker tag alpine:3.20 local-alpine:course
docker images local-alpine
output
3.20: Pulling from library/alpine
...
sha256:........ [alpine:3.20]
REPOSITORY TAG IMAGE ID CREATED SIZE
local-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.

Quick check
01Two servers both run 'docker pull myapp', a week apart, and end up running different builds of the app. What most likely happened?
Correct — Docker fills in :latest when you name no tag, and that label points at whatever was pushed to it last. Push a new image and the label moves, so the later pull hands back something different.
Incorrect — No. Connection speed changes how long the download takes, not which image arrives. Both servers ask for the same name and get whatever that name points at.
Incorrect — No. Pull is predictable. For one name and tag it always fetches the image that name currently points at, and logging in controls access rather than version.
Incorrect — No. A pulled image is read-only and stays exactly as it landed. The thing that moved was the label back on the registry.
02Docker expands nginx:1.27-alpine into the full name docker.io/library/nginx:1.27-alpine. What is the 'library' part doing in there?
Incorrect — docker.io is the registry. library comes after it.
Correct — library is Docker's account for official images, which is why plain nginx expands to library/nginx.
Incorrect — The tag is 1.27-alpine, the part after the colon. library sits well before it.
Incorrect — The image name is nginx. library is the account sitting in front of it.
03You built payments-api:1.0, ran docker tag payments-api:1.0 sachinchr/payments-api:1.0, and the push comes back with 'denied: requested access to the resource is denied.' What is wrong?
Incorrect — The image is fine. 'denied' is about permission to write to that account, not about the bytes inside the image.
Incorrect — docker tag adds a second name to the same image. It copies nothing, so there was no copy to fail.
Incorrect — A 'denied' message means the registry answered you and said no. An outage looks different, usually a timeout or a connection error.
Correct — 'denied' means the registry does not believe you may write there. Log in as sachinchr, then push.

Related