How Do You Push and Pull Docker Images From Docker Hub?
Table of Contents
Docker Hub is the default
registry. Both docker pull nginx and docker push talk to it unless you specify otherwise. The workflow consists of four commands, but most of the friction people hit is in step two.
1. Log in #
docker login
# or, for CI, non-interactively:
echo "$DOCKERHUB_TOKEN" | docker login -u "$DOCKERHUB_USER" --password-stdin
Use an access token, instead of your account password. Tokens can be scoped and revoked individually, and if you have two-factor authentication enabled (which you should), the CLI won’t accept your password at all. Credentials get cached in ~/.docker/config.json (or handed off to a credential helper like docker-credential-desktop if one’s configured), so you generally only do this once per machine.
2. Tag the image to match the target repository #
This is the step people skip and then get confused by. docker build doesn’t know where you intend to push an image, so the image has to carry the full destination name before you push it:
# note: docker build needs a Dockerfile in the current directory, otherwise these commands will cause errors
docker build -t myapp .
docker tag myapp acme/myapp:1.4.0
docker tag doesn’t copy anything; it just adds another name pointing at the same image ID locally. You can also skip the separate step by building under the final name right away (docker build -t acme/myapp:1.4.0 .). To see every name pointing at one image, use docker image inspect --format '{{.RepoTags}}' myapp.
3. Push #
docker push acme/myapp:1.4.0
Docker uploads each layer that the registry doesn’t already have (layers are deduplicated by digest, see how registries work), then writes the manifest.
4. Pull, elsewhere #
docker pull acme/myapp:1.4.0
Anyone with access (public repository, or private with matching credentials) gets the same image, as long as the tag hasn’t been re-pushed in the meantime (see What Are Docker Tags).
Rate limits #
Anonymous and free-tier authenticated pulls from Docker Hub are rate-limited per IP or per account. If you’re hitting 429 Too Many Requests in CI, that’s usually because many CI runners share an egress IP (or a whole company shares one NAT gateway) and you’re all burning the same anonymous quota.
Here are possible fixes, in order of effort:
- authenticate your pulls (authenticated limits are higher than anonymous)
- use a paid Docker Hub plan
- mirror the images you depend on to a registry you control.
The error everyone hits: denied: requested access to the resource is denied #
This almost never means “your password is wrong.” It means one of:
- You’re not logged in, or you’re logged in as a user without push access to that namespace.
docker push someoneelse/myappfails unless you actually have write access tosomeoneelse’s namespace. Docker Hub repository names aren’t a free-for-all. - You tagged the image with the wrong namespace. Pushing to
myapp:1.4.0(no namespace) tries to push tolibrary/myapp, which you don’t control, since that namespace is reserved for Docker’s official images. You almost always want<your-username-or-org>/myapp. - Your access token doesn’t allow pushing. Tokens can be limited to read-only access, so check the token’s permissions in Docker Hub settings, not just whether you’re logged in.