intermediate~3h

Docker Registry & Image Distribution: Tagging, Versioning, Push & Pull

A registry is the piece of Docker's architecture that turns a locally-built image into something every environment — CI, staging, production — can pull the exact same bytes of. This chapter is that distribution layer: Docker Hub, running your own private registry, and the tagging/versioning conventions that keep it usable at scale.

A Docker registry is a stateless, content-addressable storage and distribution service for images — it stores image layers (identified by SHA-256 content digest) and manifests (which layers make up a given tag), and serves them to any client speaking the Docker Registry HTTP API v2.

Docker Hub is the default public registry (index.docker.io), but production systems almost always also run or use a private registry — a self-hosted one (Harbor, Nexus, the open-source registry:2 image) or a cloud-managed one (Amazon ECR, Google Artifact Registry, Azure ACR, GitHub Container Registry) — for internal images that must never be publicly pullable.

An image reference like myregistry.com/team/app:1.4.2 encodes three things: which registry to talk to, which repository (namespace/name) within it, and which tag identifies a specific version of that repository's images.

  • docker push uploads each layer of an image to the registry that isn't already present there (deduplicated by content digest), then uploads a manifest listing which layers, in which order, make up the tag being pushed.

  • docker pull does the reverse: fetches the manifest for the requested tag, then downloads only the layers not already cached locally, and reassembles them into a usable image.

  • A tag (like :1.4.2 or :latest) is just a mutable pointer to one specific manifest digest — pushing a NEW image to an EXISTING tag simply repoints that tag to a new manifest, it does not create a new immutable version by itself.

  • Authentication to a private registry uses docker login, which stores a token/credential in ~/.docker/config.json, sent as a Bearer token or Basic Auth header on subsequent push/pull requests.

  • Registries support content-addressable references via digest (myapp@sha256:abc123...) alongside tag references — pulling by digest always retrieves the exact same immutable content, unlike pulling by a mutable tag.

  • Tag your local build: docker build -t myregistry.com/team/app:1.4.2 .

  • Authenticate to the target registry: docker login myregistry.com

  • Push the image: docker push myregistry.com/team/app:1.4.2

  • On another machine (CI runner, production host), pull the exact same image: docker pull myregistry.com/team/app:1.4.2

  • Verify you got exactly what was pushed by comparing digests: docker inspect --format='{{index .RepoDigests 0}}' myregistry.com/team/app:1.4.2

  • For a private registry running as a container itself: docker run -d -p 5000:5000 --name registry registry:2, then push to localhost:5000/myapp:1.0.

A CI pipeline builds an image once, tags it with the Git commit SHA (app:a1b2c3d), pushes it to a private registry, and every downstream environment (staging, production) pulls that EXACT tag — guaranteeing staging and production run byte-identical images, not two separate builds that merely share a version number.

A multi-team organization runs Harbor as an internal registry with per-team projects (harbor.internal/payments/*, harbor.internal/inventory/*), each with its own access control, so one team's credentials can't push to another team's image namespace.

A public open-source project publishes multi-architecture images (amd64 and arm64) under one tag using a manifest list, so docker pull myapp:1.0 transparently fetches the correct architecture's layers depending on which machine ran the pull.

  • Tag images with something traceable back to source — a Git commit SHA or a semantic version, never JUST :latest for anything deployed — :latest is a moving target with no way to know what code it actually contains six months later.

  • Use immutable tags in production deployment manifests (pin to :1.4.2 or a digest, not :latest), so a Kubernetes rollout can't silently pull different bytes on two different nodes if the tag gets repointed mid-rollout.

  • Run a private registry (or a private namespace on a managed one) for anything internal — never assume a 'private-sounding' repository name on a public registry is actually access-controlled.

  • Set up automated vulnerability scanning on push (Harbor, ECR, and most managed registries support this natively) so a known-CVE base image doesn't silently make it to production.

  • Configure a retention/cleanup policy — registries accumulate old tags indefinitely by default, and storage costs (plus pull-time image-listing slowness) grow unbounded without one.

Assuming ,[object Object], means 'the newest version'

— it means whatever was last pushed to that specific tag, which could be an old rollback, a broken build, or genuinely the newest thing, with no way to tell which just from the tag name.

Repointing a tag that's already deployed

— pushing a new image to a tag some environment is already running, expecting that environment to somehow pick up the change; it won't, until something explicitly re-pulls, and worse, if it DOES auto-pull (some misconfigured deployments do), you now have no record of which bytes were actually running at incident time.

Storing registry credentials in a Dockerfile or in image layers

— a RUN docker login baked into a build stage, or credentials copied into an early layer and only 'removed' in a later one, are still recoverable from the image's layer history even after a later layer deletes the file.

No cleanup policy, ever

— treating a registry as infinite, free storage, until a bill or a slow docker pull (from listing thousands of tags) forces someone to deal with years of accumulated cruft at once.

  • Layer deduplication across pushes means pushing a NEW tag that shares most layers with an EXISTING one (a small code change, same base image) only actually uploads the changed layers — structure your Dockerfile (per the earlier Dockerfile chapter) so frequently-changing layers are last, maximizing this reuse.

  • Pulling by digest instead of tag avoids one extra registry round-trip (resolving a tag to a digest) that pulling by tag requires — a small but real latency difference at scale, across thousands of pod starts in a cluster.

  • Regional/geo-replicated registries (or a pull-through cache) reduce pull latency for distributed teams/clusters — pulling a large image across a slow international link on every CI run is a common, fixable source of slow pipelines.

  • Use a registry co-located with (or geographically close to) your compute — cross-region registry pulls are a routinely overlooked source of slow deployments and CI runs.

  • Enforce image signing (Docker Content Trust, or Sigstore/cosign) in production so only cryptographically verified images can be deployed, closing the gap where a compromised registry account could push a malicious image under a trusted tag.

  • Mirror critical base images (or use a pull-through cache) so your build/deploy pipeline doesn't have a hard dependency on a third-party public registry's uptime.

  • Automate tag retention (keep last N tags per branch, delete anything untagged/dangling after a grace period) as a scheduled job, not a manual cleanup someone remembers to do eventually.

  • Stand up a local private registry with docker run -d -p 5000:5000 registry:2, push an image to it, then pull it back down on a different tag to confirm round-tripping works.

  • Tag one image three different ways (a semantic version, a Git-SHA-style tag, and :latest) and push all three — then inspect the registry's manifest listing to see that all three tags point at the SAME underlying digest.

  • Push a small change to the same image and observe (via docker push's verbose output) which layers actually upload versus which are reported as 'already exists' due to digest-based deduplication.

A registry stores content-addressable layers and manifests, and tags are just mutable pointers to a specific manifest — understanding that distinction is what prevents the single most common registry mistake, treating :latest (or any mutable tag) as a reliable version identifier. Production systems tag by commit SHA or semantic version, pull by that specific immutable reference everywhere, scan on push, and clean up aggressively — none of which is optional once more than one environment depends on getting the exact same bytes every time.

Want a visual for this concept?

Generate a diagram tailored to “Docker Registry & Image Distribution: Tagging, Versioning, Push & Pull” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to Containerized System Design: Microservices, API Gateway, Event-Driven & Deployment Architectures← Back to all Docker chapters