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 path | Purpose |
|---|---|
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/list | List 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:
| Step | Request | Result |
|---|---|---|
| 1 | GET /v2/ | 401 with a WWW-Authenticate: Bearer challenge |
| 2 | Token exchange at the realm with scope repository:<owner>/<repo>:push,pull | Bearer token |
| 3 | HEAD /v2/<owner>/<repo>/blobs/<digest> for each of the 4 blobs | 3 present, 1 missing |
| 4 | POST then PUT for the missing application layer | Blob committed |
| 5 | PUT /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.
Related Terms
- How to authenticate to a private registry in GitHub Actions: the credential options for a workflow job, and which one survives a token expiry mid run.
- Egress costs and the network boundaries image bytes cross: what a cloud provider meters when a pull leaves a region or a network.
- Publishing one image to several registries from one workflow: tagging one build for multiple hosts and keeping the digests aligned.
- OCI Distribution Specification: the upstream reference for the routes, media types, and status codes above.
- Registry token authentication: the bearer token handshake in full, including scope grammar.
- WarpBuild Docker builders documentation: builder profiles and the actions that build and push an image from a workflow.
- WarpBuild BYOC AWS configuration documentation: registry endpoint requirements when runners sit in your own AWS account.
- WarpBuild pricing: per minute rates by runner type.
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.