Container Registry

A container registry stores and serves container images and their manifests over the registry API, addressing every layer and config blob by its digest.

A container registry is a service that stores and serves container images and their manifests over the registry API, an HTTP interface that clients such as docker, podman, crane, and containerd all speak. An image in a registry is a set of content addressed blobs plus a manifest listing which blobs belong to it, so a push uploads those parts and a pull fetches whichever parts the local store is missing.

That shape explains most registry behavior. A pull of a six layer image writes six blobs, a re-tag inside one registry moves zero layer bytes, and a rebuild that changes one line at the end of a Dockerfile uploads one new layer while the rest of the image is already stored.

Definition

A container registry implements the OCI Distribution Specification, the standard that fixes the URL shapes, the media types, and the status codes a registry serves. The specification defines two object types and one grouping for them.

Blobs are opaque byte streams addressed by their digest, written as sha256: followed by 64 hex characters. Layer tarballs and the image config JSON are both blobs. Because the name is the hash of the content, a client can verify a downloaded blob without trusting the connection, and two repositories that share a base layer share one stored copy of it.

Manifests are JSON documents listing the config blob and the layer blobs of one image, in order, each entry carrying a media type, a size in bytes, and a digest. A manifest is itself addressed by the digest of its bytes. An image index (also called a manifest list) is a manifest whose entries are other manifests annotated with os and architecture, which is how one tag serves linux/amd64 and linux/arm64 from a single reference.

Repositories group the objects and give them a name. A full reference reads host[:port]/owner/repository[:tag|@digest], and the host is omitted only when the client has a default configured. Repository path components are restricted to lowercase letters, digits, and separators by the specification grammar, so an organization or repository name with capitals has to be lowercased before it becomes part of a reference.

The API surface

Every registry serves the same small set of routes. The table below lists the ones a build and deploy sequence touches.

Method and pathPurpose
GET /v2/Version check and authentication probe
HEAD /v2/<name>/blobs/<digest>Ask whether a blob is already stored before uploading it
POST /v2/<name>/blobs/uploads/Open an upload session and receive a session URL
PUT /v2/<name>/blobs/uploads/<ref>?digest=<digest>Finish an upload and commit the blob under its digest
POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repo>Link a blob that another repository on the same registry already stores
PUT /v2/<name>/manifests/<tag or digest>Publish the manifest, which makes the image resolvable
GET /v2/<name>/manifests/<tag or digest>Resolve a reference to a manifest
GET /v2/<name>/blobs/<digest>Download one blob
GET /v2/<name>/tags/listList the tags in a repository

The ordering matters in both directions. A push checks each blob with HEAD, uploads only the missing ones, and writes the manifest last, so a reference never resolves to an image whose layers are still in flight. A pull resolves the manifest first, then requests only the blobs the local content store lacks, which is why the second pull of a slightly changed image transfers far less than the first.

Authentication

The distribution specification leaves credential handling to the registry and standardizes the handshake around it. A client sends a request without credentials, receives 401 Unauthorized with a WWW-Authenticate: Bearer header naming a realm, a service, and a scope such as repository:acme/api:pull,push, exchanges its credentials at the realm for a bearer token carrying exactly that scope, and retries the original request (registry token authentication, checked on 2026-08-13).

Tokens are short lived. The durable credential sits in ~/.docker/config.json or in a credential helper on the machine, which is why a job on a machine created for that job logs in before its first pull.

What a pull moves

Bytes on the wire equal the sum of the compressed sizes of the blobs the client does not already hold, plus a manifest of a few kilobytes. Layers already present on the machine cost one HEAD each. Storage sits behind the registry, and reading it may cross a network boundary that the storage provider meters, which is covered on the egress costs page.

Example

This workflow builds an image, pushes it to a private registry, and hands the resulting digest to a second job that pulls the same bytes back. The registry here is GitHub's, and the packages permission on GITHUB_TOKEN is what authorizes the push (working with the container registry, checked on 2026-08-13).

name: image
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      digest: ${{ steps.push.outputs.digest }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: read
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - run: docker pull ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}

Four details in that file are worth reading closely.

The permissions block is asymmetric on purpose. The build job asks for packages: write because it publishes a manifest, and the deploy job asks for packages: read because it only resolves and downloads. GITHUB_TOKEN is scoped to the repository that started the run, so a workflow pulling an image published by a different repository needs a personal access token or a GitHub App installation token instead (automatic token authentication, checked on 2026-08-13).

The push writes a tag, and the deploy reads a digest. build-push-action exposes the manifest digest as a step output, so the deploy job pins the exact bytes the build produced even if another run retags main in between.

The reference is lowercased by the repository name. github.repository reproduces the owner and repository casing from GitHub, and a name containing capitals fails the repository grammar, so workflows in such repositories pass the name through a lowercasing step before building the tag.

Here is what the build job puts on the wire for a three layer image whose base layers are already stored in that registry:

StepRequestResult
1GET /v2/401 with a WWW-Authenticate: Bearer challenge
2Token exchange at the realm with scope repository:<owner>/<repo>:push,pullBearer token
3HEAD /v2/<owner>/<repo>/blobs/<digest> for each of the 4 blobs3 present, 1 missing
4POST then PUT for the missing application layerBlob committed
5PUT /v2/<owner>/<repo>/manifests/<sha>Tag now resolves

Only step 4 transfers meaningful bytes. The deploy job then runs steps 1 and 2 again with a pull scope, resolves the manifest by digest, and downloads whichever of the four blobs its local store lacks.

FAQ

What does a container registry actually store?

Two kinds of object. Blobs are opaque byte streams named by their sha256 digest, and they hold the layer tarballs and the image config JSON. Manifests are small JSON documents that list the config blob and the layer blobs belonging to one image, in order, with a media type and a size for each. A tag is a mutable label pointing at one manifest digest.

What is the difference between a tag and a digest?

A digest is the sha256 hash of the manifest bytes, so a digest reference always resolves to the same image. A tag is a name the registry lets a push reassign, so the same tag can point at different content in two runs. Referencing an image by digest in a deploy step removes the drift between what a build produced and what a deploy pulled.

Why does a workflow have to log in to a registry on every run?

Registry credentials live in the client's config.json or in a credential helper on the machine running the pull. A workflow job that starts on a fresh machine has an empty config, so the login step runs again to exchange credentials for a short lived bearer token scoped to the repository and the action requested.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.