Build Daily

Tinley Park · October 29, 2026
toolCNCF Distribution maintainers (originally Docker)applied

Distribution (container registry)

CNCF Distribution is the open-source registry server behind the `registry` image: a single Go binary that implements the OCI Distribution Spec, stores image blobs and manifests on a filesystem or object store, and serves them over HTTP to Docker, containerd, BuildKit and any other OCI client. Apache 2.0, free, a CNCF sandbox project, and the core library inside Docker Hub, GitHub Container Registry, GitLab's registry and Harbor. It fits a private registry for a homelab or a CI pipeline where a plain push/pull endpoint is enough and a UI, scanning and replication are not needed.

Updated October 17, 2026

Distribution is the open-source server that stores container images and hands them to whatever pulls them. A build pushes layers and a manifest; a Kubernetes node pulls them back by tag or digest. It is the code behind the registry image on Docker Hub and the shared library under most hosted registries, which makes it the smallest practical way to run a private registry inside a network.

What it is

The repository's main product is a registry that implements the OCI Distribution Specification. The project site describes it as a stateless server-side application that stores and distributes container images and other content. It also ships Go libraries for registry components, but the README marks those interfaces as unstable.

  • Licence: Apache 2.0.
  • Governance: Docker donated it to the CNCF, which accepted it at the Sandbox level on 26 January 2021.
  • Who builds on it: the README names Docker Hub, GitHub Container Registry, GitLab Container Registry, DigitalOcean Container Registry and Harbor as operators that use it as a core library.
  • Release line: v3.1. v3.0.0 (April 2025) was the first stable v3 after years on v2.8.x. v3.1.0 (April 2026) added tag pagination and moved to Go 1.25. v3.1.1 (May 2026) fixed CVE-2026-41888 and added S3 Express One Zone support. v3.1.2 (24 September 2026) makes the registry client verify manifest and blob content against the requested digest (CVE-2026-85747), which protects digest-pinned pulls and pull-through caches, and fixes S3 tag and catalog pagination.
  • Image: registry:3 on Docker Hub, Alpine-based, listening on port 5000, with data under /var/lib/registry.

The v3 upgrade carries breaking changes for anyone moving from v2: the oss and swift storage drivers are gone, docker/libtrust was replaced by go-jose, the standalone client package is no longer supported, and the default config path is now /etc/distribution/config.yml. A v2 deployment that mounts its config at the old /etc/docker/registry/config.yml path starts on the image's built-in dev config instead.

How it works

The registry is a stateless HTTP API in front of a storage driver. Everything it knows lives in storage as content-addressed blobs (layers and config objects, named by SHA256 digest) and manifests that list those blobs. Tags are pointers from a name to a manifest digest. Because the server keeps no state of its own, several replicas can share one object-store backend.

A push uploads blobs first and the manifest last. A pull reads the manifest first and then fetches the blobs it names. The figure follows both paths for the setup described under In the GL stack: an Argo Workflows build with BuildKit pushes, and containerd on a cluster node pulls.

How an image moves through a Distribution registry

Step 1 of 6. The workflow step asks BuildKit to build the image and push it with the git SHA as the tag.

Push uploads blobs before the manifest; pull reads the manifest before the blobs. Both use the OCI /v2/ API.

A few properties follow from this design. Layers are deduplicated across every image in the registry, because a blob is stored once per digest. A manifest cannot reference a blob that is not there, so a failed push leaves orphan blobs but never a broken tag. And deleting an image only removes references: the bytes stay on disk until garbage collection runs.

When to use it

Reach for it when:

  • You need a private push/pull endpoint for images that your own CI builds, inside a network that a hosted registry cannot or should not see.
  • You want a pull-through cache in front of Docker Hub to cut repeated downloads and rate-limit pressure.
  • You want the smallest registry that is OCI-conformant: one binary, one YAML file, one storage root.
  • You are building your own registry product and want the same core that Docker Hub and Harbor use.

Skip it when:

  • You need a UI, projects, role-based access, vulnerability scanning, signing policy or replication in the registry itself. Harbor wraps Distribution with those features.
  • You need many users with fine-grained permissions. Native auth is one htpasswd file or an external token server.
  • A hosted registry (GHCR, Docker Hub, a cloud provider's registry) is acceptable. It removes the storage, backup and patch work.

At a glance

Core concepts

  • Blob — a layer or config object, stored and addressed by its SHA256 digest.
  • Manifest — a JSON document listing a config blob and layer blobs. An index (manifest list) points at per-platform manifests.
  • Tag — a mutable name that points at one manifest digest. Pulling by digest is immutable; pulling by tag is not.
  • Storage driver — filesystem, s3, azure and gcs for real use; inmemory for tests.
  • Proxy mode — a pull-through cache for one upstream registry. Push to a proxy registry is not supported.
  • Notifications — webhook endpoints that receive push and pull events.
  • Garbage collection — an offline mark-and-sweep that deletes blobs no manifest references.

Key facts

Item Value
Current release v3.1.2 (2026-09-24)
Licence Apache 2.0
Image registry:3 (Alpine)
Default port 5000
Config path (v3) /etc/distribution/config.yml
Data path /var/lib/registry
Env overrides REGISTRY_ + config path, _ per level (e.g. REGISTRY_STORAGE_DELETE_ENABLED)
Auth options htpasswd (bcrypt only), token, silly (dev only); one provider at a time
Delete API Off by default (storage.delete.enabled: false)
Upload purging On by default: uploads older than 168h, checked every 24h
Build from source Go 1.26+ as of v3.1.2

Distribution vs Harbor vs Zot

Aspect Distribution Harbor Zot
Scope Registry API and storage only Full registry platform built on Distribution OCI-native registry
Packaging One container Several services One static binary
UI None Yes Web interface
Access control htpasswd or external token server Multi-tenant, role-based, identity integration Built-in authentication and authorization
Scanning, signing No Vulnerability analysis, content signing Security scanning
Replication No (single-upstream proxy cache) Replication across registries Mirroring
CNCF level Sandbox Graduated Sandbox

How to integrate

Run it locally with persistent storage and the delete API on:

docker run -d --name registry --restart=always \
  -p 5000:5000 \
  -v registry-data:/var/lib/registry \
  -e REGISTRY_STORAGE_DELETE_ENABLED=true \
  registry:3

A minimal v3 config, mounted at /etc/distribution/config.yml:

version: 0.1
log:
  fields:
    service: registry
storage:
  filesystem:
    rootdirectory: /var/lib/registry
  delete:
    enabled: true
  cache:
    blobdescriptor: inmemory
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

As a pull-through cache for Docker Hub, add a proxy block (one upstream per registry instance):

proxy:
  remoteurl: https://registry-1.docker.io
  ttl: 168h

K3s nodes read /etc/rancher/k3s/registries.yaml at start-up. For a plain-HTTP registry the endpoint must carry http://, or containerd defaults to HTTPS. Restart K3s on each node after a change:

mirrors:
  "registry.example.com:5001":
    endpoint:
      - "http://registry.example.com:5001"

BuildKit needs the same hint in buildkitd.toml to push over plain HTTP:

[registry."registry.example.com:5001"]
  http = true

Garbage collection, with the registry read-only or stopped:

# preview what would be removed
registry garbage-collect --dry-run /etc/distribution/config.yml
# also remove manifests that no tag points at
registry garbage-collect --delete-untagged /etc/distribution/config.yml

Set storage.maintenance.readonly.enabled: true and restart before the run, then remove it after. Pushes fail while it is set; pulls keep working.

In the GL stack

Distribution v3.1.2 runs on the management cluster and is the private registry for every image the platform builds. All three clusters reach it on port 5001. Port 5000, the registry default, is held on macOS by the AirPlay Receiver, so the registry is published one port up.

Traffic is plain HTTP. The registry is only reachable inside the platform's private network, so it runs without TLS or registry auth, and BuildKit and each cluster's container runtime are configured to treat it as an HTTP endpoint.

Images reach it from the build pipeline. Argo Workflows on the management cluster runs one workflow per product image, and BuildKit builds the image with its layer cache and pushes it here. This setup replaced local builds in Docker Desktop. Every image is tagged with the git SHA of the commit it was built from, so a tag names exactly one build and Argo CD rolls out a new version by changing that tag in the manifests it applies to the beta and prod clusters. Trivy then scans the running workload images on the beta cluster, and Kyverno policies on beta and prod include image registry rules.

Gotchas

  • Deleting needs two steps. With storage.delete.enabled: true, a DELETE by digest removes the reference only. Disk space returns after registry garbage-collect runs. Delete is off by default, so a stock registry only grows.
  • Garbage collection is stop-the-world. The docs warn that uploads during a collection can lose layers and corrupt an image. Put the registry in read-only mode or stop it first, and run --dry-run before the real pass.
  • Unique tags pile up. A tag per commit means storage grows with every build. There is no built-in tag retention; old tags must be deleted by digest by a script, and then GC run.
  • The v3 config path moved. v3 reads /etc/distribution/config.yml. A config mounted at the v2 path is silently ignored, and the registry starts on its dev defaults.
  • htpasswd needs TLS and bcrypt. Basic auth will not run over clear text, and only bcrypt hashes (htpasswd -B) are accepted. The file is read once at start-up, so new users need a restart.
  • Plain HTTP needs the scheme everywhere. K3s defaults mirror endpoints to HTTPS unless they start with http://, and BuildKit needs http = true for the host. Each client fails differently, usually as a TLS handshake error.
  • Port 5000 on a Mac. The AirPlay Receiver listens on 5000 on recent macOS releases, so a registry published there from a Mac collides with it.
  • A proxy cache is one upstream, read-only. It mirrors a single registry, push is not supported, and any upstream credentials it holds expose private images to anyone who can reach it.

Risks

  • Security exposure without auth. An unauthenticated HTTP registry lets anyone on the network pull and push. That is acceptable only when the network boundary is the control, and the boundary has to stay that way.
  • Patch cadence. v3.1.0, v3.1.1 and v3.1.2 each shipped CVE fixes in 2026. Pin the version and watch the releases.
  • Single storage root. On the filesystem driver, the registry's data is one directory. Losing it loses every image not rebuilt from source; back it up or make images reproducible from git.
  • Project health. Active, with regular releases and a CNCF home, but it remains at the Sandbox level. Lock-in is low: the API is the OCI spec, so any conformant registry can take its place.

Alternatives

  • Harbor — pick it when the registry needs a UI, projects, OIDC users, scanning, signing policy or replication. It runs Distribution inside it.
  • Zot — pick it for a single-binary, OCI-native registry that still includes auth, scanning, mirroring and a web interface. Also a CNCF sandbox project.
  • GitHub Container Registry or Docker Hub — pick a hosted registry when the images can leave the network and the storage and patch work is not worth owning.
  • Gitea or GitLab package registry — pick it when a self-hosted Git forge is already running and images should sit next to the code.

Related