Docker & ContainersXIII Β· RegistriesPrivate registries
Private registries β Distribution, Harbor, GitLab, Quay
What you'll learn
- Choose between Distribution, Harbor, GitLab and Quay on operational grounds rather than feature lists
- Deploy a Distribution registry with a storage backend you can reason about
- Probe a registry in a way that distinguishes a live API from a working pull
- Predict the blast radius of a registry outage on a running estate
Prerequisites
Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12
A private registry is the foundation of a production Docker supply chain, and it is also a stateful service sitting directly on the deploy path. Those two facts pull in different directions: the first argues for features, the second argues for something you can restore from a tarball at 03:00.
The four common options
- CNCF Distribution β the reference open-source registry, and the code most of the others are built on. Minimal: no UI, no RBAC beyond what you put in front of it, no scanning, no replication. Roughly one process and a storage backend.
- Harbor β the CNCF graduated registry. Distribution plus a Postgres database, a UI, RBAC, scanning, replication, retention policies and signing policy enforcement. Substantially more moving parts.
- GitLab Container Registry β bundled with GitLab. Namespaces track projects, permissions track GitLab permissions, CI integration is free.
- Quay β Red Hatβs registry. Clair scanning, geo-replication, mirroring from public registries, robot accounts. Available hosted (Quay.io) or self-hosted.
The honest selection criterion is not the feature matrix. It is: how many components must be restored, in what order, for a pull to work again?
| Components holding state | Restore order | |
|---|---|---|
| Distribution | Storage backend only | One step |
| GitLab registry | Registry storage + GitLab database | Database, then storage |
| Harbor | Registry storage + Postgres + Redis + config | Database and storage together, then services |
| Quay | Registry storage + database + config | Database and storage together |
Distributionβs minimalism is a feature on a bad day. Harborβs policy enforcement is a feature on every other day. Pick according to which day you are more worried about, and know the answer before you need it.
A Distribution registry you can reason about
docker run -d \
--name registry \
--restart always \
-p 5000:5000 \
-v /srv/registry:/var/lib/registry \
registry:3That single bind mount is the entire state of the service. Everything a Distribution registry knows β every layer, every manifest, every tag β lives under it. The container itself is disposable.
version: 0.1
log:
level: info
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
maintenance:
readonly:
enabled: false
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]Two keys there deserve attention now because they matter later:
storage.delete.enableddefaults to off. With it off, the registry accepts pushes forever and refuses every delete, so retention is impossible and the disk fills. Every operator discovers this the first time they try to remove an image.storage.maintenance.readonly.enabledis the switch that makes garbage collection safe. It is off here; it must be on while GC runs. That is a whole lesson of its own, and it starts here in the config.
Every key has an environment-variable equivalent formed by upper-casing the path with underscores, which is how you configure the container without maintaining a file:
docker run -d --name registry \
-e REGISTRY_STORAGE_DELETE_ENABLED=true \
-p 5000:5000 \
-v /srv/registry:/var/lib/registry \
registry:3Storage backends
The filesystem driver is the right default for a single registry with a real backup. It stops being right the moment you want two registry replicas, because two Distribution processes over one shared filesystem is a correctness question about that filesystem, not about Distribution.
For anything horizontally scaled, use an object-store driver β S3, Azure Blob, GCS. The registry becomes stateless, replicas become trivial, and the durability problem moves to a service that already solved it. The cost is latency on every blob operation and a bill that scales with pull traffic.
Probing a registry properly
βThe registry is upβ usually means βthe TCP port answeredβ, which tells
you almost nothing. The API has a defined entry point: a GET /v2/
must return 200 OK when the registry is serving and authentication is
not required, or 401 Unauthorized when it is. Anything else is a
fault.
REGISTRY=https://registry.example.com
curl -fsS -o /dev/null -w '%{http_code}\n' "$REGISTRY/v2/" || true401Illustrative output
That is liveness. It is not readiness, because a registry can answer
/v2/ perfectly while its storage backend is unreachable. The check
that actually corresponds to βa deploy will workβ is a manifest fetch
for something you know exists:
REGISTRY=registry.example.com
REPO=myorg/myapp
DIGEST=sha256:REPLACE_ME
ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json'
curl -fsSI -H "Accept: $ACCEPT" \
"https://$REGISTRY/v2/$REPO/manifests/$DIGEST" \
| grep -i 'docker-content-digest'docker-content-digest: sha256:REPLACE_MEIllustrative output
If the returned digest does not match the one you requested, stop and investigate β you are talking to something that is not serving the content you asked for.
TLS, and the flag you should not reach for
A registry must be served over TLS. When the certificate is wrong, the
daemon fails the pull with an x509 error, and there is a tempting
configuration key that makes the error go away:
docker info --format 'insecure: {{.RegistryConfig.IndexConfigs}}
mirrors: {{.RegistryConfig.Mirrors}}'insecure: map[docker.io:0x40001a2000]
mirrors: []Illustrative output
Harbor, GitLab and Quay in one paragraph each
Harbor is the standard self-hosted choice when you want policy
enforced rather than documented: per-project RBAC, tag retention rules
(configured per project in the UI or API β not in harbor.yml),
Trivy-backed scanning with a βblock pull if severity above Xβ gate, and
replication to another Harbor or to a public registry. The operational
cost is a Postgres database that is now on your restore path, and an
upgrade procedure with more steps than βpull the new imageβ.
HARBOR_VERSION=v2.15.0
cd /opt
curl -fsSLO "https://github.com/goharbor/harbor/releases/download/${HARBOR_VERSION}/harbor-offline-installer-${HARBOR_VERSION}.tgz"
tar xzf "harbor-offline-installer-${HARBOR_VERSION}.tgz"
# edit harbor/harbor.yml: hostname, TLS paths, database password
sudo ./harbor/install.shGitLab Container Registry is the lowest-marginal-cost option if you
already run GitLab: a projectβs registry namespace, permissions and CI
credentials all follow from the project itself. Configuration lives in
gitlab.rb:
registry_external_url 'https://registry.example.com'
gitlab_rails['registry_enabled'] = true
The trade is coupling. A GitLab outage or a GitLab upgrade window is now a registry outage or a registry upgrade window, and your deploys were probably not scoped into that maintenance notice.
Quay brings Clair scanning, geo-replication, repository mirroring from upstream registries, and robot accounts designed for automation. Signing in the modern Quay/OCI world is Cosign and OCI referrers; Notary v1 is being retired and its successor work moved to the Notation project, so treat any documentation referring to βNotary v2β as referring to Notation.
Replication
Replication solves two different problems and the design differs by which one you have:
- Latency. Consumers far from the registry pay a network tax on every pull. A read-only replica or pull-through cache close to them fixes it, and a cache is usually enough.
- Availability. If the registry disappearing must not stop deploys, you need a second registry that is authoritative, not a cache β and therefore a decision about how clients fail over to it.
Harbor offers scheduled, optionally bidirectional replication. Quay
offers asynchronous geo-replication. Distribution offers neither; you
put a pull-through cache in front of it or you copy images yourself
with docker buildx imagetools create, which transfers the whole
multi-platform index without pulling layers to the local host.
Knowledge check
Knowledge check Β· 5 questions
Q1. A Distribution registry accepts pushes but every attempt to delete a manifest returns 405 Method Not Allowed. What is the cause?
Q2. Your monitoring reports the registry as healthy because `GET /v2/` returns 401. A deploy still fails. What did the probe fail to test?
Q3. A registry becomes unreachable. Which of the following break? Select all that apply.
Q4. Adding a registry to `insecure-registries` is an acceptable way to accept a certificate signed by your internal CA.
Q5. A backup of a Distribution registry that captures only the `blobs/` tree restores every byte of every image with no way to name any of them.
Passing score: 75%. Answers are checked in this browser.