Registries and Docker Hub
A registry is the service that stores and distributes container images. When
you run docker pull nginx, Docker downloads from Docker Hub; when you run
docker push, it sends to the registry named in the image. This chapter covers
how image names work, authentication, and how to run your own registry.
Anatomy of an image name
registry.example.com:5000/team-a/my-api:1.4.0
└──────────┬─────────────┘ └──┬─┘ └─┬──┘ └─┬─┘
registry namespace repo tag
- registry — host (and port). If omitted, Docker assumes
docker.io(Docker Hub). - namespace/repo — organization/user and repository name. On Docker Hub,
images with no namespace (e.g.
nginx) are the Docker Official Images, which actually live indocker.io/library/nginx. - tag — a mutable label for a version. If omitted, Docker uses
latest(which is nothing special — it is just the default tag).
Equivalent examples:
docker pull nginx
docker pull nginx:latest
docker pull docker.io/library/nginx:latest
Most common registries
| Registry | Host | Notes |
|---|---|---|
| Docker Hub | docker.io |
default; has a rate limit for anonymous and free accounts |
| GitHub Container Registry | ghcr.io |
integrated with GitHub repositories/Actions |
| GitLab Container Registry | registry.gitlab.com |
one registry per GitLab project |
| Amazon ECR | <account>.dkr.ecr.<region>.amazonaws.com |
login via aws ecr get-login-password |
| Google Artifact Registry | <region>-docker.pkg.dev |
login via gcloud auth configure-docker |
| Azure ACR | <name>.azurecr.io |
az acr login |
| Quay | quay.io |
built-in vulnerability scanner |
Docker Hub rate limit
For anonymous users, Docker Hub limits the number of pulls per time window
(per IP). In CI this is easy to hit. Solutions: authenticate
(docker login), use a mirror / pull-through cache, or move the base
images to another registry.
Authentication
docker login # Docker Hub
docker login ghcr.io # another registry
docker login registry.example.com:5000
Credentials are stored in ~/.docker/config.json. By default, in plain text
(base64, not encrypted). For production/CI, use a credential helper
(docker-credential-pass, docker-credential-ecr-login, etc.) that keeps the
secret in the OS keychain or obtains short-lived tokens.
In CI, prefer an access token (revocable, scoped) over the account password:
echo "$REGISTRY_TOKEN" | docker login ghcr.io -u "$USERNAME" --password-stdin
Log out:
docker logout ghcr.io
docker pull, docker push
docker pull redis:7.4
docker image tag my-api:1.4.0 registry.example.com:5000/team-a/my-api:1.4.0
docker push registry.example.com:5000/team-a/my-api:1.4.0
You can only push to a repository where you have write permission — that is why
the name must point to your namespace/registry, not to library/....
Running your own registry
The official registry:2 image implements the Registry HTTP API V2. For a lab
or an internal cache:
docker container run -d --name registry \
-p 5000:5000 \
-v registry-data:/var/lib/registry \
registry:2
docker tag alpine:3.20 localhost:5000/alpine:3.20
docker push localhost:5000/alpine:3.20
curl -s http://localhost:5000/v2/_catalog
# {"repositories":["alpine"]}
TLS and authentication
Docker refuses push/pull over HTTP on any host other than localhost. For a
real registry you need TLS:
docker container run -d --name registry -p 443:5000 \
-v "$(pwd)"/certs:/certs \
-v registry-data:/var/lib/registry \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry:2
And basic authentication with htpasswd (bcrypt):
docker run --rm --entrypoint htpasswd httpd:2 -Bbn admin secret > auth/htpasswd
docker container run -d --name registry -p 443:5000 \
-v "$(pwd)"/auth:/auth -v "$(pwd)"/certs:/certs -v registry-data:/var/lib/registry \
-e REGISTRY_AUTH=htpasswd \
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
registry:2
Registry for development only (HTTP)
If you really need HTTP without TLS on an internal host, declare it as
insecure in each client's /etc/docker/daemon.json:
{ "insecure-registries": ["internal-registry.lab:5000"] }
sudo systemctl restart docker
Avoid this outside a lab.
Docker Hub pull-through cache
registry:2 can act as a read mirror/cache of Docker Hub, easing the rate
limit:
docker container run -d --name mirror -p 5000:5000 \
-v mirror-data:/var/lib/registry \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
registry:2
On the clients, /etc/docker/daemon.json:
{ "registry-mirrors": ["http://internal-mirror.lab:5000"] }
Self-hosted registry alternatives
- Harbor — a full registry: RBAC, built-in scanning (Trivy), signing, replication, quotas, retention. It is the de facto standard for an internal company registry.
- Zot — a lightweight OCI registry, native spec only, with optional scanning and UI.
- GitLab / Gitea / Forgejo — they ship an embedded registry alongside Git.
Cleaning up space in the registry
Deleting a tag through the API marks the manifest as removable, but the blobs only go away during garbage collection:
docker exec registry bin/registry garbage-collect /etc/docker/registry/config.yml
Enable REGISTRY_STORAGE_DELETE_ENABLED=true to allow deletion.