---
title: "07 — Container Registries: Where Images Live, Get Tagged, and Ship"
uid: container-registries
tags: ["registry", "ghcr", "ecr", "tagging", "tags", "dockerhub", "roadmap:docker", "docker"]
excerpt: "A registry is content-addressed storage for images; a tag is a movable pointer. Production should pin digests, not trust `:latest`."
date: 2026-08-13T03:28:14+0000
source: https://www.aveshina.my.id/en/blog/container-registries
---

"Docker Hub, the website" was how I conflated the registry concept with the product, which hid what a tag actually is. The model that separated them: **a registry is a content-addressed storage service for images, and a tag is just a movable pointer — so production should pin digests, not trust :latest.** [1][5]

The framing worth holding onto is that registries are the distribution layer of the Docker story. Building an image on my laptop is useless if I can't get it to the server, and copying image files by hand is a nightmare because an image is a manifest plus many layers. A registry solves this with a simple API: push an image up, pull it down anywhere, by name and tag [1].

## The registry's job

```figure
<svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="A central registry holding three stacked-layer images. On the left, a laptop pushes an image with docker push my-app:1.2.0. On the right, a production server pulls the same image with docker pull my-app:1.2.0. The tag 1.2.0 is shown as a small label arrow pointing at one image snapshot.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Laptop (push) -->
    <rect x="30" y="90" width="110" height="50" rx="8" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="85" y="112" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">laptop</text>
    <text x="85" y="128" font-size="9" fill="#475569" text-anchor="middle">docker build</text>

    <!-- Registry -->
    <rect x="250" y="40" width="220" height="160" rx="10" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="360" y="62" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">registry</text>
    <g>
      <rect x="275" y="78"  width="170" height="12" rx="3" fill="#fde68a" stroke="#ca8a04"/>
      <rect x="275" y="92"  width="170" height="12" rx="3" fill="#fde68a" stroke="#ca8a04"/>
      <rect x="275" y="106" width="170" height="12" rx="3" fill="#fde68a" stroke="#ca8a04"/>
      <text x="285" y="118" font-size="9" fill="#422006">my-app@sha256:abc…</text>
    </g>
    <g>
      <rect x="275" y="135" width="170" height="12" rx="3" fill="#fde68a" stroke="#ca8a04"/>
      <rect x="275" y="149" width="170" height="12" rx="3" fill="#fde68a" stroke="#ca8a04"/>
      <text x="285" y="174" font-size="9.5" fill="#422006">tag: 1.2.0 → this digest</text>
    </g>

    <!-- Server (pull) -->
    <rect x="580" y="90" width="110" height="50" rx="8" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="635" y="112" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">prod server</text>
    <text x="635" y="128" font-size="9" fill="#475569" text-anchor="middle">docker run</text>

    <!-- arrows -->
    <defs><marker id="rarr" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto"><path d="M0,0 L10,5 L0,10 z" fill="#64748b"/></marker></defs>
    <path d="M140,108 L248,108" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#rarr)"/>
    <text x="194" y="100" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">push</text>
    <path d="M472,128 L578,128" fill="none" stroke="#64748b" stroke-width="1.6" marker-end="url(#rarr)"/>
    <text x="525" y="120" font-size="10" font-weight="700" fill="#475569" text-anchor="middle">pull</text>
  </g>
</svg>
```

A registry stores images keyed by name plus an immutable **digest** (a SHA256 hash of the manifest). On top of that, it lets me attach human-readable **tags** — 1.2.0, alpine, latest — that point at a digest. The digest is the truth; the tag is a convenience that can be re-pointed at any time [1][5]. That distinction is the source of every tagging best practice and every tagging footgun.

## Docker Hub and the alternatives

**Docker Hub** is Docker's own public registry and the default docker push/pull destination. It hosts "official images" (the vetted nginx, postgres, node images maintained by the upstream projects or Docker), plus public and private user repositories, automated builds linked to source repos, and webhooks [2][3]. When I run docker pull nginx with no registry prefix, Docker Hub is where it comes from.

But Hub is one choice. The major cloud platforms each ship a managed registry tightly integrated with their own ecosystem [4]:

- **GitHub Container Registry (GHCR)** — natural fit when my CI is GitHub Actions; images live next to the code.
- **Amazon ECR** — integrated with IAM and ECS/EKS on AWS.
- **Google Artifact Registry** — the successor to GCR, integrated with GCP.
- **Azure Container Registry** — the Azure equivalent.

There's also the open-source **Distribution** registry (the project behind Docker Hub's API) that I can self-host for fully private, air-gapped setups [1]. The choice between them is almost never technical — the registry API is standardized — it's about which cloud I'm already in and where my IAM lives.

## Tagging: the label you learn to control

If the digest is truth and the tag is a movable label, then tagging strategy is about *which labels I move where, and when*. The best practices the roadmap collects all flow from that [5][6]:

- **Pin explicit versions in production.** my-app:1.2.0 is honest. my-app:latest is ambiguous — it's whatever was last pushed under that name, which means two deploys a week apart can run different code with no version bump [5].
- **Use semantic versioning for releases.** 1.2.0 conveys breaking/minor/patch; latest conveys nothing.
- **Include build metadata in CI tags.** Tagging with the git commit hash or build number (1.2.0-abc1234) makes every image traceable back to the exact source that produced it.
- **Automate tagging in CI/CD.** Manual tagging is where typos and "I forgot to bump it" live. The pipeline should tag on every merge.
- **Clean up old tags.** Registries accumulate thousands of layers if I let them; most managed registries have lifecycle policies to prune untagged or old images.

The rule I internalized: in dev, :latest is convenient; in anything that resembles production, I pin a real version and treat the tag as a contract.

## Runtime configuration: how the image runs

A neighboring idea the roadmap groups with registries is **runtime configuration** — the options that shape how an image behaves when it starts [7]. These don't change the image; they shape the container produced from it:

- **Environment variables** (-e KEY=value) — the Twelve-Factor config mechanism.
- **Resource limits** (--cpus, --memory) — cgroup ceilings, the topic of an earlier post.
- **Restart policy** (--restart unless-stopped) — whether the daemon brings the container back after a crash or reboot.
- **Network and port settings** (--network, -p) — covered in the CLI post.
- **Logging drivers** (--log-driver) — where stdout goes.

The unifying idea: the image is immutable, but the *container* is shaped at run time by these flags or, more manageably, by a Compose file. The same image can run as a dev container (no limits, logs to the terminal) and a prod container (memory-capped, logs shipped to a central store), purely from runtime config [7].

## How I use this

The payoff is a default set of habits. Every stateful project I ship gets pushed to a registry the moment it builds in CI, tagged with the git short SHA plus a semver on release, and the production manifest references that explicit tag — never :latest. I pick the registry that matches the cloud I'm already in, because the IAM and lifecycle tools come for free. And when a deploy behaves differently from the previous one, the first thing I check is the image digest the running container actually resolved to, because the tag is a label and the digest is the truth. Treating tags as movable pointers, not promises, is the one habit that has saved me the most debugging time.

## References

[1] Docker, Inc., "Docker Registry," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/registry/](https://docs.docker.com/registry/)

[2] Docker, Inc., "Docker Hub," hub.docker.com, 2024. [Online]. Available: [https://hub.docker.com/](https://hub.docker.com/)

[3] Docker, Inc., "Docker Hub repositories," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/docker-hub/repos/](https://docs.docker.com/docker-hub/repos/)

[4] GitHub, "About GitHub Container Registry," GitHub Docs, 2024. [Online]. Available: [https://docs.github.com/en/packages/guides/about-github-container-registry](https://docs.github.com/en/packages/guides/about-github-container-registry)

[5] Docker, Inc., "Build, tag, and publish an image," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/get-started/docker-concepts/building-images/build-tag-and-publish-an-image/](https://docs.docker.com/get-started/docker-concepts/building-images/build-tag-and-publish-an-image/)

[6] S. Preston, "Semantic Versioning 2.0.0," semver.org, 2024. [Online]. Available: [https://semver.org/](https://semver.org/)

[7] Docker, Inc., "docker run reference," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/engine/reference/run/](https://docs.docker.com/engine/reference/run/)

```quiz
Q: Which statement about image digests vs tags is correct?
- A digest is a movable label; a tag is an immutable hash
- A digest is an immutable SHA256 hash of the manifest; a tag is a movable pointer that can be re-pointed
correct: 1
explain: The digest is the truth — it addresses a specific image manifest. The tag is a convenience label that can be moved to point at a different digest at any time.

Q: Why is `:latest` considered unsafe for production references?
- It is reserved for internal Docker use only
- It is whatever was last pushed under that name, so two deploys can run different code with no version bump
correct: 1
explain: `:latest` carries no version information and can be silently overwritten. Production should pin an explicit semver tag or, better, the digest.

Q: What is the main reason to choose GHCR over Docker Hub for a project on GitHub?
- GHCR images are smaller
- It lives next to the code and integrates natively with GitHub Actions IAM, so CI can push without separate credentials
correct: 1
explain: The registry API is standardized; the choice is about ecosystem fit. GHCR co-locates images with source and uses GitHub's existing auth, which is the natural fit for GitHub-based CI.

Q: Tagging a release image as `1.2.0-abc1234` (semver + git short SHA) is an example of…
- making the image traceable back to the exact source commit that produced it
- hiding the version from end users
correct: 0
explain: Embedding the commit hash in the tag makes every deployed image traceable to its source, which is the whole point of CI-driven tagging.

Q: Runtime configuration options (env vars, --memory, --restart)…
- are baked into the image at build time and cannot change between deploys
- shape the container at run time without changing the image, so one image can run differently per environment
correct: 1
explain: Runtime config is applied by docker run or a Compose file. The image stays immutable; the container's behavior is tailored per environment through flags and env vars.
```
