Docker & Containers — Level by Level›06 · Registries: tag, push & pull

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.

Beginner → Practitioner
Key wordsdocker pushdocker pulldocker tagdocker loginimage digestDocker HubAmazon ECRHarborconfig.jsoncredential helpertoomanyrequests
Build host / CI docker build docker tag registry.lab.local:5000 TLS certificate htpasswd login /v2/ API Any other host docker pull docker run docker login 1 docker push 2 docker login 3 docker pull 4 image name registry.lab.local:5000/shop/api:1.4 the host part of the name tells Docker which registry to use
Tag with the registry's name, log in, push once; any host that can log in can pull.

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't localhost, Docker assumes Docker Hub (docker.io).
  • Repository: the path. Official images on Docker Hub live under library/, so nginx really means docker.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:

  1. Mirror the base images you use into your own registry and build from there.
  2. Run a pull-through cache (next lesson) and point the daemon at it with registry-mirrors (Level 4).
  3. 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

  1. Create a free Docker Hub account and an access token (Account settings → Personal access tokens).
  2. docker login -u <you> and paste the token.
  3. docker tag alpine:3.20 <you>/hello:1 and docker push <you>/hello:1.
  4. docker rmi <you>/hello:1 alpine:3.20, then docker pull <you>/hello:1.
  5. Look at ~/.docker/config.json, then docker 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.