A container registry stores images by content hash. A tag is only a pointer at one, and somebody can move it. Start there and the rest follows.
Highlights
- A container registry is a content addressed store rather than a file server, which means every layer is identified by a hash of what it contains.
- A tag is a mutable pointer and a digest is the image itself, and almost every confusing registry behaviour traces back to that difference.
- Pulling the same tag on two different days can give you two different images, which is why a build that worked yesterday can fail today with nothing changed.
- Docker Hub limits anonymous pulls to 100 per six hours attributed by IP address, so everyone behind one office connection or NAT gateway shares that budget.
- A multi stage Dockerfile with two base images counts as two pulls, which is how a modest pipeline exhausts an anonymous quota before lunch.
- A pull through cache is the definitive fix for rate limits, because it fetches an image once and serves it locally after that.
- Rate limits fail intermittently and look like flaky tests, which is what makes them expensive to diagnose rather than merely annoying.
A build that passed yesterday fails today. Nobody changed the Dockerfile, nobody changed the pipeline, and the base image tag is identical. The reason is usually that the tag is identical and the image behind it is not, because a tag is a pointer somebody else is free to move. That single fact explains more registry confusion than anything else, and it is rarely the first thing anyone is taught.
Superficially it looks like storage for images, and treating it that way works right up until it does not. It is closer to a content addressed database, where everything is identified by a hash of what it contains and tags are convenience labels layered on top. This guide starts with what is actually stored, spends real time on the tag and digest distinction because everything else follows from it, then covers the practical work: pushing, pulling, authentication, rate limits, scanning, and retention.
What a container registry is
A container registry is a service that stores and distributes container images, holding each image as a set of compressed layers plus a manifest describing them, and identifying everything by a cryptographic hash of its contents so that the same content always has the same identifier.
The phrase content addressed is doing the work in that sentence. Nothing is stored under a name you chose. Everything is stored under a hash of itself, and names are pointers added afterwards.
What actually gets stored
An image is not one file. It is three kinds of thing, and knowing that makes caching, pull behaviour, and storage costs all make sense.
Layers are compressed archives of filesystem changes. Each instruction in your Dockerfile that modifies the filesystem produces one. They are stored separately, identified by hash, and shared across every image that contains the same layer.
A config object holds the metadata that is not filesystem content, meaning environment variables, the entrypoint, the working directory, the architecture, and the ordered list of layer hashes.
A manifest ties it together, listing the config and the layers by hash. The manifest is what a tag actually points at, and the hash of the manifest is the image digest.
That structure explains something people notice and rarely explain. When you push a second image built on the same base, the push is fast, because the registry already has those layers and only accepts the ones whose hashes it has not seen. Layer sharing is not an optimisation bolted on top, it falls out of storing everything by content hash.
Did you know
A multi architecture image adds one more level of indirection. The tag points at a manifest list rather than a manifest, and that list maps each platform to the manifest for that platform, so arm64 and amd64 are separate images under one tag. This is why pulling the same tag on an Apple laptop and on a cloud runner can give you genuinely different images, entirely correctly. It is also why building on one architecture and deploying to another produces the exec format error that catches nearly everyone once.
Tags and digests, which is the whole thing
This section is the one worth reading twice.
A tag is a human readable label pointing at a manifest. It is mutable. Anyone with push access can move it to a different manifest at any time, and nothing about the tag records that this happened.
Its counterpart, the digest, is the SHA256 hash of the manifest. It is immutable by construction, because changing any byte of the content changes the hash. The same digest always means exactly the same bytes.
docker pull python:3.12-slim
docker inspect --format='{{index .RepoDigests 0}}' python:3.12-slim
docker pull python@sha256:REPLACE_WITH_THE_DIGEST_YOU_JUST_PRINTEDThe first command pulls by tag, which is what almost everyone does. The second asks Docker what digest that tag actually resolved to, which is the piece of information that makes a build reproducible. The third pulls by digest instead, and that command will fetch the identical image in six months regardless of what has happened to the tag in the meantime.
Three consequences follow, and each is worth stating on its own.
A tag is not a version. It is a bookmark. python:3.12-slim receives security updates, which is genuinely desirable and also means the image changes under a stable name.
latest is the least informative tag available. It carries no meaning beyond being whatever was pushed most recently without another tag, and it is not automatically anything.
Deploying by tag means your running version is not fully determined by your manifest. Two clusters applying the identical YAML at different times can run different images.
Watch out
The mystery where a build worked yesterday and fails today with no changes on your side is almost always a moved tag. Somebody updated the upstream base image, the tag now points at a new manifest, and a dependency inside it changed. Pin base images by digest in anything you need to reproduce, and record the digest in your build output so the question is answerable later. The habit costs one line and converts an unexplainable failure into a diff you can read.
The push and pull flow
Knowing the sequence makes error messages legible instead of cryptic.
Authenticating exchanges your credentials for a short lived token scoped to a repository and an action. This is why a pull can succeed and a push fail with the same login, and why a token that worked an hour ago may need refreshing.
Pulling starts by asking the registry what digest a tag resolves to, then requesting the manifest, then requesting only the layers not already present locally. A cached layer is not transferred at all.
Pushing works in the opposite order. The client offers each layer's hash, the registry says which ones it already has, only the new ones upload, and the manifest is written last. Writing the manifest last is what makes a push atomic, since the tag does not resolve to the new image until every layer it references is present.
docker login registry.example.com
docker build -t registry.example.com/team/api:2.4.0 .
docker push registry.example.com/team/api:2.4.0
docker manifest inspect registry.example.com/team/api:2.4.0The build command tags the image with the full registry path, which is required, because a name without a registry prefix defaults to Docker Hub. The manifest inspect command shows you what the registry now holds for that tag, including the layer list and the digest, without pulling anything.
Quick tip
Asking a registry what digest a tag points at is a lightweight request that does not transfer any layers, which makes it a cheap way to detect that an upstream image has changed. Recording the digest your build resolved to, then comparing on the next build, turns a silent base image update into a visible one. Some tooling does this for you, and doing it manually is a few lines in a pipeline.
Build the container fundamentals
Registries make far more sense once layers, builds, and image structure are comfortable rather than abstract. The Docker for the Absolute Beginner course on KodeKloud covers images, containers, Dockerfiles, and registries with hands on labs, which is exactly the ground this guide builds on.
Which registry to use
Four broad options, and the differences matter less than teams expect once the basics are right.
Two things are worth knowing beyond the table. Harbor is a CNCF graduated project and the usual answer when self hosting is genuinely required, offering project level access control, quotas, replication between registries, a built in proxy cache, and integrated scanning. And self hosting a registry means running something that every deployment and every build depends on, which is a tier zero service, so budget for that honestly rather than treating it as a side project.
Rate limits, and the failure that looks like flakiness
This is the operational problem most teams meet first, and its shape is worth understanding because the symptom is misleading.
Docker Hub limits anonymous pulls to 100 per six hours, attributed by IPv4 address or IPv6 subnet rather than by user. Authenticating with a free account roughly doubles that, and paid tiers lift it subject to fair use. The attribution detail is the important part, because everyone behind a single office connection, NAT gateway, or shared runner pool draws from the same budget.
Two details make this bite harder than the numbers suggest. A pull counts each image, so a multi stage Dockerfile with two different base images counts as two. And a cold runner has no local cache, so a pipeline that starts fresh containers pulls everything every time.
In the wild
Rate limits do not fail cleanly, which is what makes them expensive. They fail intermittently, they fail for whoever happens to run a build after the quota is exhausted, and they look exactly like flaky tests. A team can spend a week chasing a nondeterministic pipeline failure that is really a shared pull budget being consumed by a different team on the same network. If your builds fail unpredictably at the image pull step and succeed on retry, check the quota before you check anything else.
Three fixes exist and they are not equivalent.
Authenticate. The single cheapest improvement, and it takes one credential in your pipeline.
Pin and cache aggressively. Pinning by digest and reusing layers between runs reduces how many pulls happen at all.
Run a pull through cache. This is the definitive fix. A pull through cache is a registry configured to fetch from upstream on a miss and serve locally on a hit, so an image is fetched once and served from your network afterwards. Every major registry supports this in some form, and it removes the dependency rather than raising the ceiling.
A tagging strategy that works
Most tagging problems come from using one tag for two purposes. Separate them and the awkwardness disappears.
An immutable version tag identifies exactly one build and never moves. A semantic version, a build number, or a commit SHA all work, and the commit SHA has the advantage of being unambiguous and automatic.
A moving convenience tag points at whatever is current for humans and local development, such as latest or staging. Useful for people, never referenced by production.
docker build \
-t registry.example.com/team/api:2.4.0 \
-t registry.example.com/team/api:sha-$(git rev-parse --short HEAD) \
-t registry.example.com/team/api:latest .
docker push --all-tags registry.example.com/team/apiBuilding once with three tags means all three refer to the same digest, which is the point. The version tag is what production references, the commit tag makes it trivial to trace a running image back to the exact source that produced it, and latest stays convenient for anyone pulling by hand.
Two registry settings are worth enabling if yours supports them. Tag immutability rejects a push that would move an existing tag, which turns an accidental overwrite into an error rather than a mystery. Retention policies delete untagged and old images automatically, which matters because a registry left alone grows without limit and layer storage is a real cost.
Watch out
Deleting a tag does not necessarily free storage. Layers are shared by content hash, so a layer only becomes reclaimable once no manifest anywhere references it, and reclaiming it requires garbage collection which some registries run on a schedule and some require you to trigger. If your registry bill grows after you cleaned things up, the deletion probably worked and the garbage collection has not run yet.
Scanning and provenance
Registries also host two security capabilities worth turning on early.
Vulnerability scanning compares the packages in your layers against known advisory data and reports what it finds. The important thing is where you enforce it. Scanning on push tells you what you built, scanning continuously tells you when a previously clean image becomes vulnerable because a new advisory was published, and blocking on scan results in your pipeline is what turns information into a control. Most teams enable the report and never enable the gate.
Signing and attestation let you verify that an image came from your pipeline rather than merely being present in your registry. A signature proves origin, and an attestation records how the image was built and from what. This matters more than it used to, because push access to a registry is push access to your production images, and the digest is what a signature covers, which is another reason to deploy by digest.
Practise all of this on a real cluster
Registry behaviour becomes obvious the moment you push twice and watch the second push skip layers it already has. The Certified Kubernetes Administrator (CKA) course on KodeKloud covers image pull policies, secrets for private registries, and how pods actually obtain images, and the KodeKloud playgrounds give you environments to push, pull, and break things freely.
Common mistakes
Where to start
- Find out which registry your production images actually come from, since many teams discover an unexpected public dependency.
- Check whether any pipeline pulls anonymously, and add authentication, which is the cheapest single improvement available.
- Print the digest that one of your base image tags currently resolves to, and record it in your build output.
- Pin one base image by digest and see whether anything breaks, because that tells you how much drift you had.
- Turn on tag immutability if your registry supports it, so an accidental overwrite becomes an error.
- Add a retention policy for untagged images, then check whether garbage collection runs automatically.
- If you scan images already, turn the report into a gate on one severity level rather than leaving it advisory.
Conclusion
A container registry is easy to use badly because it looks like storage and behaves like a content addressed database. Layers are identified by hash and shared automatically, a manifest lists them, and a digest is the hash of that manifest. Tags are convenient labels sitting on top of all this, and they can move.
That single distinction between a moving tag and an immutable digest explains most of what confuses people. Builds are not reproducible because the tag changed. Two environments differ because they resolved the same tag at different times. A signature is meaningful because it covers a digest rather than a name.
So print the digest your base image tag currently resolves to this week, and record it somewhere your build output keeps. It takes one line, it costs nothing, and it turns the next unexplainable build failure into something you can actually diff.
Ready to Get Comfortable With Containers End to End?
Registries sit between building images and running them, so understanding both sides is what makes the middle make sense. The Docker for the Absolute Beginner course on KodeKloud covers images, layers, Dockerfiles, and registries with hands on labs, the Certified Kubernetes Administrator (CKA) course covers how clusters pull and authenticate, and the KodeKloud playgrounds give you somewhere to experiment freely. Start with one today.
FAQs
Q1: What is a container registry, and how is it different from a repository?
A container registry is the service that stores and distributes container images, while a repository is a named collection of images inside it. So registry.example.com is the registry, team/api is a repository within it, and 2.4.0 is a tag on an image in that repository. What makes a registry more interesting than storage is that it is content addressed, meaning every layer and every manifest is identified by a cryptographic hash of its own contents rather than by a name you chose. An image is stored as a set of compressed layers, a config object holding metadata such as the entrypoint and environment, and a manifest listing both by hash. Names are pointers added on top of that structure. This is why pushing an image built on a base you have already pushed is fast, since the registry only accepts layers whose hashes it has not seen before, and layer sharing falls out of the storage model rather than being an optimisation added later.
Q2: What is the difference between a tag and a digest, and why does it matter?
A tag is a human readable label pointing at an image manifest, and it is mutable, so anyone with push access can move it to different content at any time with nothing recording that it happened. A digest is the SHA256 hash of the manifest itself, which makes it immutable by construction, since changing any byte changes the hash. The same digest always means exactly the same bytes. This matters in three concrete ways. A tag is not a version, it is a bookmark, so python:3.12-slim receives security updates and genuinely changes under a stable name. A build that passed yesterday and fails today with nothing changed on your side is usually a moved base image tag. And deploying by tag means your running version is not fully determined by your manifest, so two clusters applying identical YAML at different times can run different images. The fix is to pin by digest anywhere reproducibility matters, and to record the digest your build resolved to so the question is answerable afterwards.
Q3: What do I need to know before using a container registry properly?
Less than you might think, and most of it is conceptual. Understand that an image is layers plus a config plus a manifest, all identified by content hash, because that explains caching, push speed, and storage behaviour. Understand the tag and digest distinction, which is the single most useful thing in this guide. Know that authentication produces a short lived token scoped to a repository and an action, which is why a pull can succeed while a push fails on the same login. And know that an image name without a registry prefix defaults to Docker Hub, which is how teams end up with an unintended public dependency. On the practical side, docker login, docker push, docker pull, and docker manifest inspect cover nearly everything. For the foundations underneath, the Docker for the Absolute Beginner course on KodeKloud covers images, layers, and Dockerfiles with hands on labs.
Q4: Why do I keep hitting Docker Hub rate limits, and what fixes it?
Because the limits are attributed by network address rather than by person, so everyone behind one office connection, NAT gateway, or shared runner pool draws from the same budget. Docker Hub allows 100 anonymous pulls per six hours attributed by IPv4 address or IPv6 subnet, authenticating with a free account roughly doubles that, and paid tiers lift it under a fair use policy. Two details make it bite harder than the numbers suggest. Each image counts, so a multi stage Dockerfile with two different base images is two pulls. And a cold runner has no local cache, so a pipeline that starts fresh containers pulls everything every run. Three fixes exist in increasing order of effectiveness. Authenticate, which is a single credential and the cheapest improvement available. Pin by digest and reuse layers between runs, which reduces how many pulls occur. And run a pull through cache, which fetches an image once from upstream and serves it locally afterwards, removing the dependency rather than raising the ceiling. Worth knowing: rate limits fail intermittently and look exactly like flaky tests, so check the quota before chasing nondeterminism.
Q5: Should I use latest, and what tagging strategy should I follow?
Reserve latest for local convenience and never reference it from production, because it carries no meaning beyond being whatever was pushed most recently without another tag. Most tagging problems come from using one tag for two purposes, so separate them. Use an immutable version tag that identifies exactly one build and never moves, which can be a semantic version, a build number, or a commit SHA, with the commit SHA having the advantage of being unambiguous and automatic. Use a moving convenience tag such as latest or staging for humans and local development. Build once and apply all the tags you want, so every one refers to the same digest. Two registry settings are worth enabling where available. Tag immutability rejects a push that would move an existing tag, turning an accidental overwrite into an error rather than a mystery. And retention policies expire untagged and old images automatically, which matters because a registry left alone grows without limit. Note that deleting a tag does not immediately free storage, since a layer becomes reclaimable only when no manifest references it and reclaiming requires garbage collection.
Q6: What security should I turn on in my registry?
Three things, in rough order of value. Vulnerability scanning compares the packages in your layers against advisory data, and the important decision is where you enforce it rather than whether you enable it, since scanning on push tells you what you built, continuous scanning tells you when a previously clean image becomes vulnerable as new advisories land, and blocking on results in your pipeline is what converts information into a control. Most teams enable the report and never enable the gate. Second, replace long lived registry credentials in CI with short lived tokens obtained through workload identity, because a leaked static token grants push access to your production images, which is among the most valuable credentials you hold. Third, signing and attestation let you verify that an image came from your pipeline rather than merely being present in your registry, with a signature proving origin and an attestation recording how it was built. Signatures cover the digest rather than the tag, which is one more reason to deploy by digest, since a signature on a name that can be moved proves considerably less than one on immutable content.
Discussion