Lesson 06 of 13 · Level 3 — Registries
Registries: tag, push & pull
Understand exactly what an image name means, then tag, push and pull images from Docker Hub, Amazon ECR and Harbor. Log in safely, pin images by digest, and deal with rate limits and 'denied' errors.
Reading an image name
Every image reference has up to four parts:
registry.lab.local:5000 / shop/api : 1.4 @sha256:9f2c...
└──── registry ────────┘ └ repo ┘ └tag┘ └── digest ──┘
- Registry: the host (and port). If the first part has no
.or:and isn'tlocalhost, Docker assumes Docker Hub (docker.io). - Repository: the path. Official images on Docker Hub live under
library/, songinxreally meansdocker.io/library/nginx. - Tag: a human label. If you leave it out, you get
latest, which is just a tag name, not "the newest". - Digest: the SHA-256 of the image manifest. Immutable.
A tag is a bookmark: someone can move it to a different page. A digest is the page's fingerprint: if a single letter on the page changes, the fingerprint doesn't match any more.
Pull
$ docker pull nginx:1.27
1.27: Pulling from library/nginx
a2318d6c47ec: Pull complete
...
Digest: sha256:<digest>
Status: Downloaded newer image for nginx:1.27
docker.io/library/nginx:1.27
Docker downloads only the layers it doesn't already have. Multi-architecture images (amd64, arm64) share one tag; Docker picks the variant that matches the host.
Tag and push
Pushing is naming the image after the registry, then pushing that name:
$ docker build -t shop/api:1.4 .
$ docker tag shop/api:1.4 registry.lab.local:5000/shop/api:1.4
$ docker login registry.lab.local:5000
Username: ci-bot
Password:
Login Succeeded
$ docker push registry.lab.local:5000/shop/api:1.4
The push refers to repository [registry.lab.local:5000/shop/api]
5f70bf18a086: Pushed
...
1.4: digest: sha256:<digest> size: 1784
docker tag doesn't copy anything; it adds a second name to the same image ID. Push the same image under several tags (1.4, 1.4.2, sha-3f1c9a0) and only the first push uploads layers.
Common registries and how you log in:
| Registry | Name looks like | Login |
|---|---|---|
| Docker Hub | docker.io/<user>/app:tag |
docker login (use an access token, not your password) |
| GitHub Container Registry | ghcr.io/<org>/app:tag |
token with write:packages via --password-stdin |
| Amazon ECR | <acct>.dkr.ecr.<region>.amazonaws.com/app:tag |
aws ecr get-login-password piped into docker login -u AWS --password-stdin (token valid 12 h, see the cheat sheet) |
| Harbor / self-hosted | harbor.lab.local/<project>/app:tag |
docker login harbor.lab.local (robot accounts for CI) |
CI logins
Use --password-stdin so the token doesn't appear in the process list or shell history, use a robot/service account with push rights to only the repositories it needs, and prefer short-lived tokens (ECR's 12-hour token, OIDC-based cloud logins) over long-lived passwords.
Where logins are stored
docker login saves credentials in ~/.docker/config.json. Without a credential helper, they're only base64-encoded, which anyone who can read the file can decode:
{
"auths": {
"registry.lab.local:5000": { "auth": "Y2ktYm90OnMzY3IzdA==" }
}
}
Better: configure a credential helper so the secret goes to the OS keychain or a cloud helper fetches tokens on demand:
{
"credsStore": "pass",
"credHelpers": {
"123456789012.dkr.ecr.eu-west-1.amazonaws.com": "ecr-login"
}
}
Pin by digest
A tag can be re-pushed to point at a different image. For production, deploy by digest (or tag + digest):
$ docker buildx imagetools inspect registry.lab.local:5000/shop/api:1.4 | grep Digest
Digest: sha256:<digest>
$ docker pull registry.lab.local:5000/shop/api:1.4@sha256:<digest>
In Compose and Kubernetes manifests, image: registry.lab.local:5000/shop/api:1.4@sha256:<digest> keeps the readable tag and guarantees the content.
Rate limits and air-gapped hosts
Docker Hub limits how many pulls anonymous users can make per IP address in a time window, with higher limits for logged-in and paid accounts; the exact numbers change, so check Docker's current documentation. Symptoms are toomanyrequests errors, often in CI where many jobs share one outbound IP. Fixes, best first:
- Mirror the base images you use into your own registry and build from there.
- Run a pull-through cache (next lesson) and point the daemon at it with
registry-mirrors(Level 4). - At minimum, log in before pulling in CI.
For hosts with no registry access at all, move images as files:
$ docker save -o shop-api-1.4.tar registry.lab.local:5000/shop/api:1.4
$ scp shop-api-1.4.tar edge-host:/tmp/
$ ssh edge-host docker load -i /tmp/shop-api-1.4.tar
Try it: Docker Hub round trip
- Create a free Docker Hub account and an access token (Account settings → Personal access tokens).
docker login -u <you>and paste the token.docker tag alpine:3.20 <you>/hello:1anddocker push <you>/hello:1.docker rmi <you>/hello:1 alpine:3.20, thendocker pull <you>/hello:1.- Look at
~/.docker/config.json, thendocker logout.
Going deeper: what a push actually sends
An image is a manifest (JSON listing a config blob and layer blobs by digest) plus the blobs. A push uploads missing blobs first, then the manifest; a pull fetches the manifest, then the blobs it doesn't have. Multi-architecture images add an index (manifest list) pointing to one manifest per platform. This is the OCI distribution API, the same for Docker Hub, ECR, Harbor and the open-source registry, which is why crane, skopeo and oras can copy images between registries without a Docker daemon at all:
$ skopeo copy docker://docker.io/library/nginx:1.27 docker://registry.lab.local:5000/mirror/nginx:1.27
Recap
- The host part of the name picks the registry; no host means Docker Hub.
- Tag with the registry name, login, push. Only missing layers move.
- Credentials sit in
~/.docker/config.json: use a credential helper and--password-stdin. - Deploy by digest for immutability. Beat rate limits with your own mirror or a pull-through cache.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.